TypeScript 大挑战(二)

101 阅读3分钟

这是 TypeScript 大挑战系列 的第 2 篇,笔者计划用 6 个月时间完成 type-challenges 项目中的 133 个挑战并记录下自己的收获,目前还剩余 130 个,截止日期为 2023 年 1 月25 日。

挑战进度

4. 数组第一个元素

挑战内容

实现一个通用 First<T>,它接受一个数组 T 并返回它第一个元素的类型。

// TODO: 补充 First 代码
type First<T extends any[]> = any

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // 为 'a'
type head2 = First<arr2> // 为 3
type head3 = First<[]] // 为 never

知识点:infer / never

题目解析

为了取得数组第一个元素的类型,自然而然的想法就是通过 T[0] 这种下标的方式访问,但是这样会存在一个问题:对于空数组,得到的类型为 undefined 而不是 never,例如:

type First<T extends any[]> = T[0]

type head = First<[]> // 为 undefined

然而题目要求对于空数组返回 never,这也是更合理的行为,因为此时的确不存在第一个元素(对应 never 的含义),那我们该怎么做呢?

可能有同学会想出 type First<T extends any[]> = T['length'] >= 1 ? T[0] : never 这样的尝试,但是 T['length'] 拿到的其实是长度 number 这个类型,而不是 T 的长度这个值,number 这个类型是没办法直接和值 1 进行比较的。

像这种在条件语句中拿到待推断类型的场景,就需要用到 infer 了。它和 extends 搭配在一起,能够在 X extends Y 为 true 的情况下,使用通过 infer 推断得到的类型,例如:

type ArrayElementType<T>
  = T extends (infer E)[] ? E : never;

// item1 为 number
type item1 = ArrayElementType<number[]>

// item2 为 never
type item2 = ArrayElementType<number>

题目答案

关键点在于将 any[] 解构成 [infer Head, ...any[]],这样就可以拿到第一个元素的类型,并在 Head 不存在的时候返回 never:

type First<T extends any[]>
  = T extends [infer Head, ...any[]] 
    ? Head : never

5. 获取元组长度

挑战内容

创建一个通用的 Length,接受一个 readonly 的数组,返回这个数组的长度:

// TODO: 实现 Length
type Length<T> = any

type tesla = ['tesla']
type spaceX = [
  'FALCON 9',
  'FALCON HEAVY',
]

// teslaLength 为 1
type teslaLength = Length<tesla>

// spaceXLength 为 2
type spaceXLength = Length<spaceX>

知识点:T['length'] / readonly

题目解析

在上一题 4. 数组第一个元素 已经提到:可以通过 T['length'] 拿到数组类型 T 的长度。

同时,在大挑战(一)中也提到:type tesla = ['tesla'] 这样固定不变的元组可以用 readonly 来修饰。

题目答案

type Length<T extends readonly any[] >
  = T['length']

6. Exclude

挑战内容

实现内置的 Exclude <T, U> 类型,从联合类型 T 中排除 U 的类型成员,来构造一个新的类型:

// TODO: 实现 MyExclude
type MyExclude<T, U> = any

type Result = MyExclude<
  'a' | 'b' | 'c' | 'd', 
  'a' | 'd'
> // 结果为 'b' | 'c'

知识点:联合类型 / extends

题目解析

对于 T 这样的联合类型而言,作用于 extends 时,有一条独特的规则:

When conditional types act on a generic type, they become distributive when given a union type.

翻译一下就是:

X extends Y 的条件类型语句中,X 是联合类型的范型,则会将联合类型的每一个可能的值代入进行独立计算,再将结果通过 | 组合起来。

举个例子,ToArray<string | number> 的结果其实是 ToArray<string> | ToArray<number

type ToArray<Type>
  = Type extends any ? Type[] : never

// StrArrOrNumArr 为 string[] | number[]
type StrArrOrNumArr = ToArray<
  string | number
>

有一点需要强调的是:当且仅当 联合类型的范型 时才会有这种效果,如果单单是联合类型而不涉及泛型时,则是直接拿字面量进行判断:

type IsString<T>
  = T extends string ? true : false

// isA 为 true | false
type isA = IsString<string | number>

// isB 为 false,此时不涉及泛型
type isB
  = (string | number) extends string
    ? true : false

题目答案

由于 T 是联合类型,所以可以将 T 中的每一个类型取出,判断其是否在 U 中,如果在的话则通过 never 排除,不在的话则保留:

type MyExclude<T, U>
  = T extends U ? never : T

结语

在完成了大挑战(一)之后,笔者明显感觉进行大挑战(二)的时候速度提升了,不知道大家是否也有这样的感觉呢🤔