在TS中对类型进行运算,变量使用泛型参数来表示,循环使用递归来实现。在 TS 中泛型主要有3个主要用途:

  1. 声明泛型容器或组件,例如各种容器类 Map、Array、Set等
  2. 对类型进行约束,例如使用extends约束传入参数符合某种特定结构
  3. 生成新的类型,例如 ReturnType

    泛型的常见使用方式

    假如实现一个泛型链表
    1. class LinkedList<T> {
    2. // 泛型类
    3. value: T
    4. next?: LinkedList<T> // 可以使用自身进行类型声明
    5. constructor(value: T, next?: LinkedList<T>) {
    6. this.value = value
    7. this.next = next
    8. }
    9. log() {
    10. if (this.next) {
    11. this.next.log()
    12. }
    13. console.log(this.value)
    14. }
    15. }
    16. let list: LinkedList<number> // 泛型特化为number
    17. ;[1, 2, 3].forEach((value) => {
    18. list = new LinkedList(value, list)
    19. })
    20. list.log()
    这里我们使用泛型来实现类的自定义。再者,我们也可以使用泛型来对 React 组件来进行约束:
    1. function Form<T extends { [key: string]: any }>({ data }: { data: T }) {
    2. return (
    3. <form>
    4. {data.map((value, key) => <input name={key} value={value} />)}
    5. </form>
    6. )
    7. }
    这里,我们使用泛型来对组件来进行约束,对传入的数据规定特定的结构。

    类型运算

    Q:编写一个 TS 泛型工具 Transfer,实现将 Fn 参数中的最后一个参数切去。希望达到以下的效果: ```typescript function inputFn(a: number, b: string, c: boolean) { return a }

type OutputFn = Transfer // 希望返回类型 (a: number, b: string) => number

  1. 首先我们需要创造一些工具类型:
  2. <a name="V8JMx"></a>
  3. ### 获取函数入参类型
  4. ```typescript
  5. type ArgumentType<Fn> = Fn extends (...args: infer T) => any ? T : never

这个工具函数使用三元运算符和 infer 来获取函数的入参类型。

操作元组

  1. type Unshift<T extends any[], U> = ((_0: U, ..._1: T) => any)
  2. extends ((..._: infer R) => any)
  3. ? R : never

这里我们使用函数、三元操作符和 infer 来获取添加到元组前面之后的元组类型。类似的,如果要切掉函数中第一个参数,我们也可以使用这种方法

  1. type Shift<T extends any[]> = ((...args: T) => any)
  2. extends ((_0: any, ..._1: infer R) => any)
  3. ? R : never

操作递归

在对类型进行操作时,不仅需要描述递归还需要写明递归的终止条件。例如实现一个加法类型操作:

  1. type Inc = {
  2. [n: number]: number
  3. 0: 1
  4. 1: 2
  5. 2: 3
  6. 3: 4
  7. }
  8. type Dec = {
  9. [n: number]: number
  10. 0: -1
  11. 1: 0
  12. 2: 1
  13. 3: 2
  14. }
  15. // 当 T 被 U 限制时返回1,执行终止操作,反之则返回0,执行递归操作
  16. type Matches<T, U> = T extends U ? '1' : '0'
  17. // 递归的同时,修改 T 和 U 的值
  18. type Add<T extends number, U extends number> = {
  19. 1: T,
  20. 0: Add<Inc<T>, Dec<U>>
  21. }[Matches<U, 0>]

这里我们主要看 Add 这个类型,我们在1这个分支写清楚终止的条件,在0这个分支来执行递归操作,辅之以 Matches 这个类型来决定当前执行哪个条件语句。

具体实现

Shift 操作我们已经实现,至于如何实现 Pop,我们可以利用递归来对 Shift 来进行操作,反转一下来实现。

  1. type Pop<T extends any[]> = Reverse<Shift<Reverse<T>>>

这里我们使用了两次 reverse 来完成 pop 的操作。至于 Reverse ,我们可以利用递归的方式来进行实现:

  1. type Reverse<T extends any[], U extends any[] = []> = {
  2. 0: U
  3. 1: ((..._: T) => any) extends ((_0: infer First, ..._1: Res) => any)
  4. ? Reverse<Res, Unshift<U, First>>
  5. : never
  6. }[T extends [any, ...any[]]] ? 1 : 0

这样我们就完成了最开始提到的问题:

  1. type Transfer<Fn extends (...args: any[]) => any> =
  2. (...args: Pop<ArgumentType<Fn>>) => ReturnType<Fn>

之所以需要迂回这么多次,归根到底还是 TS 的不完善造成的。但递归实现的思想是非常值得借鉴的。

使用数组长度来计数

创建一定长度的数组

  1. type CreateArr<Len, El, Arr extends El[] = []> = Arr['length'] extends Len
  2. ? Arr
  3. : CreateArr<Len, El, [El, ...Arr]>

在这里我们需要传入三个参数,数组的长度和三元操作符用来判定递归的终止条件,数组 Arr 用来进行计数,当满足目标长度时,直接返回 Arr,不满足时再进行递归操作。

实现累加操作

  1. type Add<A extends number, B extends number> = [...createArr<A, 1>, ...createArr<B, 1>]['length']

使用构建数组的长度来进行计数,从而实现累加操作。

将字符串重复特定次数

  1. type RepeatStr<Str extends string, Count extends number, Arr extends Str[] = [], ResStr extends string = ''>
  2. = Arr['length'] extends Count
  3. ? ResStr
  4. : RepeatStr<Str, Count, [Str, ...Arr], `${Str}${ResStr}`>

同之前的例子类似,我们的入参是其实字符和重复的次数。Arr 用来对重复的次数进行计数,ResStr 完成对数据的计算。这里使用的是字符串操作符。

解析函数名

  1. type AlphaChars = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'
  2. | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'
  3. type TempParseResult<Token extends string, Rest extends string> = {
  4. token: Token,
  5. rest: Rest
  6. }
  7. type ParseFunName<SourceStr extends string, Res extends string = ''> =
  8. SourceStr extends `${infer PrefixChar}${infer ResStr}`
  9. ? PrefixChar extends AlphaChars
  10. ? ParseFunName<ResStr, `${Res}${PrefixChar}`>
  11. : TempParseResult<Res, SourceStr>
  12. : never

这里我们使用到了字符串的识别操作,当字符串满足某个条件时对原始字符串进行截取,然后不断改变入参的值来改变操作数,直到最后不满足某个条件时再结束递归。

过滤对象类型中的数字属性值

  1. type FilterNumberProp<T extends Object> = {
  2. [K in keyof T]: T[K] extends number ? T[K] : never
  3. }[keyof T]

这里我们使用 keyof 来取得对象的 key 值,然后再用 extends 对其做类型判断,若存在则直接返回,反之则返回 never。

小结

TS 中的常见类型操作:

  • 使用三元操作符做类型判断,作为递归的终止条件
  • 使用递归完成循环操作
  • 可以对对象进行操作,包括获得 key 值(keyof T)以及取属性值 T[K]
  • 可以构造字符串 ${a}${b}, 使用字符串的模式匹配来取子串 str extends ${infer x}${infer y}

    总结

    截止当前的时间点 TS v4.4.2,很多功能都已经完善,可以使用比较简单的方式来实现。 ```typescript type Unshift = [U, …T]

type Shift = T extends [first: any, …args: infer R] ? R : never

type Push = […T, U]

type Pop = T extends […args: infer R, last: any] ? R : never

  1. 类似的递归操作的理念我们还可以在对数组进行拍扁操作中见到:
  2. ```typescript
  3. type Flatten<T> = T extends any[] ? Flatten<T[number]> : T
  4. type Example = Flatten<[number, [string, [boolean]]]> // string | number | boolean

以及实现 suffix 操作,输入[1, 2, 3, 4, 5, 6]和3,输出[3, 4, 5, 6]

  1. type Suffix<
  2. A extends any[],
  3. S extends number,
  4. C extends any[] = []
  5. > = A extends []
  6. ? []
  7. : C['length'] extends S
  8. ? A
  9. : A extends [infer F, ...infer R]
  10. ? Suffix<R, S, [...C, F]>
  11. : never

这个例子也用了递归的方式,A 作为输入的起始数组,S 作为起始点,而 C 则是我们引入的额外参数,用来执行判断。这里的多个三元表达式的出现仅仅是因为 TS 类型可支持的操作非常有限,但即便如此我们也可以利用仅有的工具来实现我们的操作。整体的思路就是不断的缩减 A 中元素的数量,同时将多出来的元素放到我们新增的数组 C 中,当 C 数组的长度和入参 S 相等的时候,我们再返回此时操作过后的 A 数组。

第5行,我们要先行判断输入的数组是否是一个空数组,若为空数组则直接将其返回。然后第7行我们再设置递归的终止条件,当 C 数组的长度和入参 S 相等时我们返回 A。接下来,我们来看具体对数组进行操作的地方,第 9 行,我们使用 infer 和 … 操作符来取出入参数组中的第一个元素 F,而减去一个元素的剩余数组作为下一次递归操作的入参,同时我们增加将 F 元素添加到 C 数组中,即第 10 行 Suffix<R, S, [...C, F]>。最后我们再使用 never 作为兜底类型。