在TS中对类型进行运算,变量使用泛型参数来表示,循环使用递归来实现。在 TS 中泛型主要有3个主要用途:
- 声明泛型容器或组件,例如各种容器类 Map、Array、Set等
- 对类型进行约束,例如使用extends约束传入参数符合某种特定结构
- 生成新的类型,例如 ReturnType
泛型的常见使用方式
假如实现一个泛型链表
这里我们使用泛型来实现类的自定义。再者,我们也可以使用泛型来对 React 组件来进行约束:class LinkedList<T> {
// 泛型类
value: T
next?: LinkedList<T> // 可以使用自身进行类型声明
constructor(value: T, next?: LinkedList<T>) {
this.value = value
this.next = next
}
log() {
if (this.next) {
this.next.log()
}
console.log(this.value)
}
}
let list: LinkedList<number> // 泛型特化为number
;[1, 2, 3].forEach((value) => {
list = new LinkedList(value, list)
})
list.log()
这里,我们使用泛型来对组件来进行约束,对传入的数据规定特定的结构。function Form<T extends { [key: string]: any }>({ data }: { data: T }) {
return (
<form>
{data.map((value, key) => <input name={key} value={value} />)}
</form>
)
}
类型运算
Q:编写一个 TS 泛型工具 Transfer,实现将 Fn 参数中的最后一个参数切去。希望达到以下的效果: ```typescript function inputFn(a: number, b: string, c: boolean) { return a }
type OutputFn = Transfer
首先我们需要创造一些工具类型:
<a name="V8JMx"></a>
### 获取函数入参类型
```typescript
type ArgumentType<Fn> = Fn extends (...args: infer T) => any ? T : never
这个工具函数使用三元运算符和 infer 来获取函数的入参类型。
操作元组
type Unshift<T extends any[], U> = ((_0: U, ..._1: T) => any)
extends ((..._: infer R) => any)
? R : never
这里我们使用函数、三元操作符和 infer 来获取添加到元组前面之后的元组类型。类似的,如果要切掉函数中第一个参数,我们也可以使用这种方法
type Shift<T extends any[]> = ((...args: T) => any)
extends ((_0: any, ..._1: infer R) => any)
? R : never
操作递归
在对类型进行操作时,不仅需要描述递归还需要写明递归的终止条件。例如实现一个加法类型操作:
type Inc = {
[n: number]: number
0: 1
1: 2
2: 3
3: 4
}
type Dec = {
[n: number]: number
0: -1
1: 0
2: 1
3: 2
}
// 当 T 被 U 限制时返回1,执行终止操作,反之则返回0,执行递归操作
type Matches<T, U> = T extends U ? '1' : '0'
// 递归的同时,修改 T 和 U 的值
type Add<T extends number, U extends number> = {
1: T,
0: Add<Inc<T>, Dec<U>>
}[Matches<U, 0>]
这里我们主要看 Add 这个类型,我们在1这个分支写清楚终止的条件,在0这个分支来执行递归操作,辅之以 Matches 这个类型来决定当前执行哪个条件语句。
具体实现
Shift 操作我们已经实现,至于如何实现 Pop,我们可以利用递归来对 Shift 来进行操作,反转一下来实现。
type Pop<T extends any[]> = Reverse<Shift<Reverse<T>>>
这里我们使用了两次 reverse 来完成 pop 的操作。至于 Reverse ,我们可以利用递归的方式来进行实现:
type Reverse<T extends any[], U extends any[] = []> = {
0: U
1: ((..._: T) => any) extends ((_0: infer First, ..._1: Res) => any)
? Reverse<Res, Unshift<U, First>>
: never
}[T extends [any, ...any[]]] ? 1 : 0
这样我们就完成了最开始提到的问题:
type Transfer<Fn extends (...args: any[]) => any> =
(...args: Pop<ArgumentType<Fn>>) => ReturnType<Fn>
之所以需要迂回这么多次,归根到底还是 TS 的不完善造成的。但递归实现的思想是非常值得借鉴的。
使用数组长度来计数
创建一定长度的数组
type CreateArr<Len, El, Arr extends El[] = []> = Arr['length'] extends Len
? Arr
: CreateArr<Len, El, [El, ...Arr]>
在这里我们需要传入三个参数,数组的长度和三元操作符用来判定递归的终止条件,数组 Arr 用来进行计数,当满足目标长度时,直接返回 Arr,不满足时再进行递归操作。
实现累加操作
type Add<A extends number, B extends number> = [...createArr<A, 1>, ...createArr<B, 1>]['length']
将字符串重复特定次数
type RepeatStr<Str extends string, Count extends number, Arr extends Str[] = [], ResStr extends string = ''>
= Arr['length'] extends Count
? ResStr
: RepeatStr<Str, Count, [Str, ...Arr], `${Str}${ResStr}`>
同之前的例子类似,我们的入参是其实字符和重复的次数。Arr 用来对重复的次数进行计数,ResStr 完成对数据的计算。这里使用的是字符串操作符。
解析函数名
type AlphaChars = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'
| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'
type TempParseResult<Token extends string, Rest extends string> = {
token: Token,
rest: Rest
}
type ParseFunName<SourceStr extends string, Res extends string = ''> =
SourceStr extends `${infer PrefixChar}${infer ResStr}`
? PrefixChar extends AlphaChars
? ParseFunName<ResStr, `${Res}${PrefixChar}`>
: TempParseResult<Res, SourceStr>
: never
这里我们使用到了字符串的识别操作,当字符串满足某个条件时对原始字符串进行截取,然后不断改变入参的值来改变操作数,直到最后不满足某个条件时再结束递归。
过滤对象类型中的数字属性值
type FilterNumberProp<T extends Object> = {
[K in keyof T]: T[K] extends number ? T[K] : never
}[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
type Push
type Pop
类似的递归操作的理念我们还可以在对数组进行拍扁操作中见到:
```typescript
type Flatten<T> = T extends any[] ? Flatten<T[number]> : T
type Example = Flatten<[number, [string, [boolean]]]> // string | number | boolean
以及实现 suffix 操作,输入[1, 2, 3, 4, 5, 6]和3,输出[3, 4, 5, 6]
type Suffix<
A extends any[],
S extends number,
C extends any[] = []
> = A extends []
? []
: C['length'] extends S
? A
: A extends [infer F, ...infer R]
? Suffix<R, S, [...C, F]>
: 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
作为兜底类型。