题目
在类型系统里实现 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'>;
原题链接
思路
类比 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,但是 true 和 boolean 并不是相等的。
实现
综上所述,最终的类型工具 Includes 实现为:
type Includes<T extends Array<unknown>, U> =
T extends [infer First, ...infer Rest] ?
(Equal<First, U> extends true ? true : Includes<Rest, U>)
:
false;