类型体操
TypeScript 的类型编程代码可能看起来有些复杂,但实际上,这些逻辑用 JavaScript 编写时大家都会。之所以在类型体操中显得困难,主要是因为大家还不熟悉一些常用的技巧和模式。
因此,从这一节开始,我们将一起学习一些类型体操的常用套路。通过掌握这些技巧,你将会发现编写各种复杂的类型逻辑变得轻松自如。这些内容也受到了神光老师文章的启发。
套路一:模式匹配做提取
模式匹配
比如这样一个 Promise 类型:
type p = Promise<'guang'>;
我们想提取 value 的类型,可以这样做:
type GetValueType<P> = P extends Promise<infer Value> ? Value : never;
解释
-
泛型参数
P:P是一个泛型参数,表示传入的类型。P需要满足Promise<infer Value>这个条件。
-
条件类型
P extends Promise<infer Value>:extends在这里用于类型检查,确保P是一个Promise类型。infer Value用于从Promise类型中推断出解析值的类型Value。
-
条件类型的结果:
- 如果
P是一个Promise类型,并且可以从中推断出解析值的类型Value,则返回Value。 - 否则,返回
never类型,表示没有匹配到任何值。
- 如果
通过 extends 对传入的类型参数 P 做模式匹配,其中值的类型是需要提取的,通过 infer 声明一个局部变量 Value 来保存,如果匹配,就返回匹配到的 Value,否则就返回 never 代表没匹配到。
// 示例 2: Promise<number>
type NumberPromiseValue = GetValueType<Promise<number>>; // number
// 示例 3: Promise<{ name: string }>
type ObjectPromiseValue = GetValueType<Promise<{ name: string }>>; // { name: string }
// 示例 4: 不是 Promise 类型
type NonPromiseValue = GetValueType<string>; // never
type StringPromiseValue = GetValueType<Promise<string>>; // string
Promise<string>是一个Promise类型,解析值的类型是string。- 因此,
StringPromiseValue的类型是string。
这就是 Typescript 类型的模式匹配:
Typescript 类型的模式匹配是通过 extends 对类型参数做匹配,结果保存到通过 infer 声明的局部类型变量里,如果匹配就能从该局部变量里拿到提取出的类型。
这个模式匹配的套路有多有用呢?我们来看下在数组、字符串、函数、构造器等类型里的应用。
数组类型
First
数组类型想提取第一个元素的类型怎么做呢?
type arr = [1,2,3]
用它来匹配一个模式类型,提取第一个元素的类型到通过 infer 声明的局部变量里返回。
type GetFirst<Arr extends unknown[]> =
Arr extends [infer First, ...unknown[]] ? First : never;
解释
-
泛型参数
Arr extends unknown[]:Arr是一个泛型参数,表示传入的类型必须是一个数组或元组类型。extends unknown[]确保Arr是一个数组或元组类型。
-
条件类型
Arr extends [infer First, ...unknown[]]:[infer First, ...unknown[]]是一个元组类型的模式匹配。infer First用于从数组或元组的第一个元素中推断出其类型First。...unknown[]表示数组或元组的剩余部分,可以是任意类型的数组。
-
条件类型的结果:
- 如果
Arr是一个非空数组或元组,并且可以从中推断出第一个元素的类型First,则返回First。 - 否则,返回
never类型,表示没有匹配到任何值。
- 如果
类型参数 Arr 通过 extends 约束为只能是数组类型,数组元素是 unkown 也就是可以是任何值。
any 和 unknown 的区别: any 和 unknown 都代表任意类型,但是 unknown 只能接收任意类型的值,而 any 除了可以接收任意类型的值,也可以赋值给任意类型(除了 never)。类型体操中经常用 unknown 接受和匹配任何类型,而很少把任何类型赋值给某个类型变量。
对 Arr 做模式匹配,把我们要提取的第一个元素的类型放到通过 infer 声明的 First 局部变量里,后面的元素可以是任何类型,用 unknown 接收,然后把局部变量 First 返回。
// 示例 1: 数组 [1, 2, 3]
type FirstNumber = GetFirst<[1, 2, 3]>; // number
// 示例 2: 元组 ['a', 'b', 'c']
type FirstString = GetFirst<['a', 'b', 'c']>; // string
字符串类型
字符串类型也同样可以做模式匹配,匹配一个模式字符串,把需要提取的部分放到 infer 声明的局部变量里。
StartsWith
判断字符串是否以某个前缀开头,也是通过模式匹配:
type StartsWith<Str extends string, Prefix extends string> =
Str extends `${Prefix}${string}` ? true : false;
解释
-
泛型参数
Str extends string和Prefix extends string:Str是一个泛型参数,表示传入的类型必须是一个字符串。Prefix也是一个泛型参数,表示传入的前缀必须是一个字符串。
-
条件类型
Str extends𝑃𝑟𝑒𝑓𝑖𝑥Prefix{string}` :${Prefix}${string}是一个模板字符串类型的模式匹配。${Prefix}表示字符串Str必须以Prefix开头。${string}表示字符串Str的剩余部分可以是任意字符串。
-
条件类型的结果:
- 如果
Str匹配到${Prefix}${string}这个模式,则返回true。 - 否则,返回
false。
- 如果
// 示例 1: 字符串以指定前缀开头
type IsStartsWithHello = StartsWith<'Hello World', 'Hello'>; // true
// 示例 2: 字符串不以指定前缀开头
type IsStartsWithHi = StartsWith<'Hello World', 'Hi'>; // false
需要声明字符串 Str、匹配的前缀 Prefix 两个类型参数,它们都是 string。
用 Str 去匹配一个模式类型,模式类型的前缀是 Prefix,后面是任意的 string,如果匹配返回 true,否则返回 false。
函数
函数同样也可以做类型匹配,比如提取参数、返回值的类型。
GetParameters
函数类型可以通过模式匹配来提取参数的类型:
type GetParameters<Func extends Function> =
Func extends (...args: infer Args) => unknown ? Args : never;
// 示例 2: 函数有一个参数
type OneParam = GetParameters<(a: number) => void>; // [number]
// 示例 3: 函数有两个参数
type TwoParams = GetParameters<(a: number, b: string) => void>; // [number, string]
解释
-
泛型参数
Func extends Function:Func是一个泛型参数,表示传入的类型必须是一个函数类型。extends Function确保Func是一个函数类型。
-
条件类型
Func extends (...args: infer Args) => unknown:(...args: infer Args) => unknown是一个函数类型的模式匹配。...args: infer Args表示函数可以接受任意数量和类型的参数,并且尝试从这些参数中推断出参数列表的类型Args。=> unknown表示函数的返回值类型可以是任何类型,因为我们只关心参数列表的类型。
-
条件类型的结果:
- 如果
Func是一个函数类型,并且可以从中推断出参数列表的类型Args,则返回Args。 - 否则,返回
never类型,表示没有匹配到任何值。
- 如果
类型参数 Func 是要匹配的函数类型,通过 extends 约束为 Function。
Func 和模式类型做匹配,参数类型放到用 infer 声明的局部变量 Args 里,返回值可以是任何类型,用 unknown。
返回提取到的参数类型 Args。
GetReturnType
能提取参数类型,同样也可以提取返回值类型:
type GetReturnType<Func extends Function> =
Func extends (...args: any[]) => infer ReturnType
? ReturnType : never;
// 示例 1: 函数返回 number
type ReturnNumber = GetReturnType<() => number>; // number
// 示例 2: 函数返回 string
type ReturnString = GetReturnType<(name: string) => string>; // string
解释
-
泛型参数
Func extends Function:Func是一个泛型参数,表示传入的类型必须是一个函数类型。extends Function确保Func是一个函数类型。
-
条件类型
Func extends (...args: any[]) => infer ReturnType:(...args: any[]) => infer ReturnType是一个函数类型的模式匹配。...args: any[]表示函数可以接受任意数量和类型的参数。infer ReturnType用于从函数类型中推断出返回值的类型ReturnType。
-
条件类型的结果:
- 如果
Func是一个函数类型,并且可以从中推断出返回值的类型ReturnType,则返回ReturnType。 - 否则,返回
never类型,表示没有匹配到任何值。
- 如果
索引类型
索引类型也同样可以用模式匹配提取某个索引的值的类型,这个用的也挺多的,比如 React 的 index.d.ts 里的 PropsWithRef 的高级类型,就是通过模式匹配提取了 ref 的值的类型:
我们简化一下那个高级类型,提取 Props 里 ref 的类型:
GetRefProps
我们同样通过模式匹配的方式提取 ref 的值的类型:
type GetRefProps<Props> =
'ref' extends keyof Props
? Props extends { ref?: infer Value | undefined}
? Value
: never
: never;
类型参数 Props 为待处理的类型。
通过 keyof Props 取出 Props 的所有索引构成的联合类型,判断下 ref 是否在其中,也就是 'ref' extends keyof Props。
为什么要做这个判断,上面注释里写了:
在 ts3.0 里面如果没有对应的索引,Obj[Key] 返回的是 {} 而不是 never,所以这样做下兼容处理。
如果有 ref 这个索引的话,就通过 infer 提取 Value 的类型返回,否则返回 never。
总结
就像字符串可以匹配一个模式串提取子组一样,TypeScript 类型也可以匹配一个模式类型提取某个部分的类型。
TypeScript 类型的模式匹配是通过类型 extends 一个模式类型,把需要提取的部分放到通过 infer 声明的局部变量里,后面可以从这个局部变量拿到类型做各种后续处理。
模式匹配的套路在数组、字符串、函数、构造器、索引类型、Promise 等类型中都有大量的应用,掌握好这个套路能提升很大一截类型体操水平。