「TS类型体操00898」实现 Includes

413 阅读3分钟

题目

在类型系统里实现 JavaScript 的 Array.includes 方法,这个类型接受两个参数,如果第1个参数包含了第2个参数,那么返回true,否则返回false

例如:

// 'Dio' 不在数组中,所以返回类型 false
type t1 = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'>;
// 'Kars' 在数组中,所以返回类型 true
type t2 = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Kars'>;

原题链接

实现 Includes

思路

类比 JavaScript

类比 JavaScript,就是实现一个函数:

这个函数接收 2参数,第 1 个参数是 1 个数组,第 2 个参数是 1 个值。

遍历数组,检查是否存在于数组里面,如果就返回 true,如果不是就返回 false

function myIncludes(array, item) {
    if (!Array.isArray(array)) {
        throw new TypeError(`第1个参数必须是数组!`);
    }
    
    for (let i = 0, len = array.length; i < len; i++) {
        if (array[i] === item) {
            return true;
        }
    }
    
    return false;
}

但是这有一个问题,遍历数组我只学过这样的形式:

type t1<T extends unknown[]> = { [K in keyof T]: K };

这样的形式确实可以遍历数组,但是最后只能返回对象类型,而题目要求返回布尔类型

所以,我需要一种新的遍历数组的方式 ---- 递归

递归遍历数组

递归遍历数组JavaScript 实现,就是实现一个递归函数,从主函数开始,递归地的调用它。实现如下:

function myIncludes(arr, target) {
    // 定义递归函数
    function proc(rangeArr, target) {
        // base case
        if (rangeArr.length === 0) {
            return false;
        }
        // 解构出`首元素`和`剩余元素组成的数组`
        const [first, ...rest] = rangeArr;
        // 如果`首元素`是目标则返回true,
        // 否则继续在`剩余元素组成的数组`中查找,返回查找结果。
        return first === target ? true : proc(rest, target);
    }
    // 从主函数开始调用递归函数
    return proc(arr, target);
}

提取逻辑点

1. 约束第 1 个参数类型必须是数组类型。
2. 解构出`首元素`和`剩余元素组成的数组`。
3. 递归调用

再把逻辑点翻译成 TypeScript 即可。

翻译成 TypeScript

约束第 1 个参数类型必须是数组类型

通过 extends 来约束类型:

type Includes<T extends Array<unknown>, U> = ...

解构出首元素剩余元素组成的数组

这要通过 infer 关键字,推断出数组解构出的类型:

type Includes<T extends Array<unknown>, U> = T extends [infer First, ...infer Rest] ...用解构出的 First类型 和 Rest类型 编写后续代码...

其中 First 是数组首元素的类型,Rest 是除了首元素以外,剩余元素组成的数组的字面量类型。

递归调用

通过在 Includes 里再调用 Includes 实现递归,每次传入的数组类型都是上次剩余的,直到数组类型空数组类型,实现在整个数组范围内查找目标:

import type { Equal } from '@type-challenges/utils';

type Includes<T extends Array<unknown>, U> = 
  T extends [infer First, ...infer Rest] ? 
    (Equal<First, U> extends true ? true : Includes<Rest, U>) 
    : 
    false;

判断相等

需要注意的一点是,在判断数组某个元素是否和目标相等时,不能用 extends,而应该用 Equal 工具。因为 extends 仅仅是继承关系,比如 true extends boolean 返回 true,但是 trueboolean 并不是相等的。

实现

综上所述,最终的类型工具 Includes 实现为:

type Includes<T extends Array<unknown>, U> = 
  T extends [infer First, ...infer Rest] ? 
    (Equal<First, U> extends true ? true : Includes<Rest, U>) 
    : 
    false;