TypeScript 条件类型 infer
typescript TypeA extends TypeB ? TrueType : FalseType
当 `TypeA` 可以赋值给 `TypeB` 时,结果为 `TrueType`,否则为 `FalseType`。这为类型编程提供了基础的分支能力。
```typescript
type IsString<T> = T extends string ? "yes" : "no"
type A = IsString<string> // "yes"
type B = IsString<number> // "no"
条件类型与泛型结合使用时,会展现出分布式条件类型的特性:如果传入的是一个联合类型,则条件类型会分别作用于每个成员,并将结果再次联合。
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number> // string[] | number[]
infer 关键字:在条件中声明类型变量
infer 只能在条件类型的 extends 子句中使用,其作用是在进行类型匹配时临时引入一个类型变量,以便后续在 TrueType 分支中使用该变量。换句话说,infer 让我们可以“捕获”类型中的某一部分,犹如在模式匹配中提取片段。
基本语法
type ExtractSomething<T> = T extends SomePattern<infer R> ? R : never
这里 infer R 表示:如果 T 满足 SomePattern 的结构,则推断出一个类型变量 R,并在真分支中返回它。
经典应用:提取函数返回值类型
TypeScript 标准库中内置的 ReturnType<T> 就是通过 infer 实现的。它能获取函数类型的返回值类型。
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
function foo() { return 42 }
type FooReturn = MyReturnType<typeof foo> // number
当 T 是一个函数类型时,infer R 会捕获其返回类型,并作为结果返回。
提取函数参数类型
Parameters<T> 可提取函数所有参数的类型,返回一个元组。
type MyParameters<T> = T extends (...args: infer P) => any ? P : never
function bar(a: string, b: number) {}
type BarParams = MyParameters<typeof bar> // [string, number]
infer P 捕获了整个参数列表,因此返回的是一个元组类型。
提取数组或元组元素类型
可以轻松获取数组内部元素的类型:
type ElementType<T> = T extends (infer U)[] ? U : never
type StrElement = ElementType<string[]> // string
type NumElement = ElementType<number[]> // number
// 对于元组同样有效
type TupleElement = ElementType<[boolean, string]> // boolean | string
由于条件类型的分布式特性,联合类型会被展开处理,因此 [boolean, string] 满足 (infer U)[] 时会匹配为 U = boolean | string(实际上,元组会被视为数组并捕获联合类型)。
对于更精确地处理元组,可以直接推断整个元组结构:
type FirstElement<T> = T extends [infer F, ...any[]] ? F : never
type First = FirstElement<[1, 2, 3]> // 1
提取 Promise 包裹的值
infer 常用于解开嵌套类型,例如实现一个 Awaited<T> 的简化版:
type MyAwaited<T> = T extends Promise<infer V> ? MyAwaited<V> : T
type Unwrapped = MyAwaited<Promise<Promise<number>>> // number
递归地提取 Promise 的值类型,直到不再满足 Promise<infer V> 模式为止。
在模板字面量类型中使用 infer
TypeScript 4.1 引入了模板字面量类型,infer 可以用来解析字符串结构,实现类似正则表达式的匹配。
type GetterName<T> = T extends `get${infer Rest}` ? Rest : never
type Name = GetterName<"getUserName"> // "UserName"
结合条件类型与递归,还能更复杂地解析:
type SplitBySep<S extends string, Sep extends string> =
S extends `${infer Head}${Sep}${infer Tail}`
? [Head, ...SplitBySep<Tail, Sep>]
: [S]
type Parts = SplitBySep<"a-b-c", "-"> // ["a", "b", "c"]
带有约束的 infer 推断
从 TypeScript 4.7 开始,infer 可以添加 extends 约束,限定推断出的类型必须满足某些条件。如果推断出的类型不满足约束,则条件类型会落到 false 分支。
type ExtractNumber<T> = T extends { value: infer V extends number } ? V : never
type Good = ExtractNumber<{ value: 42 }> // 42
type Bad = ExtractNumber<{ value: "hi" }> // never
这里 infer V extends number 表示仅当从 value 推断出的类型可赋值给 number 时才匹配成功,否则直接进入 never 分支。相比无约束的 infer V(始终匹配但得到不理想类型),约束版会更精确地过滤。
注意与技巧
分布式条件类型的陷阱
如果泛型参数为裸类型(即直接出现在 extends 左侧的未包装类型),条件类型会自动分布。若不想分布,可以将类型包装在元组中:
type NonDistributive<T> = [T] extends [any] ? true : false
结合 infer 时,通常希望利用分布式特性来提取联合类型中的每一个成员类型。例如提取联合类型中每个函数共同的部分:
type AllReturnTypes<T> = T extends (...args: any[]) => infer R ? R : never
type Fns = (() => string) | (() => number)
type Returns = AllReturnTypes<Fns> // string | number
infer 位置对推断的影响
infer 出现在协变位置(如返回值、数组元素)时会生成联合类型;出现在逆变位置(如函数参数)时会生成交叉类型。示例如下:
type Covariant<T> = T extends { a: infer U; b: infer U } ? U : never
// 对于 { a: string, b: number },U 被推断为 string | number
type Contravariant<T> = T extends { a: (x: infer U) => void; b: (y: infer U) => void } ? U : never
// 对于 { a: (x: string) => void; b: (x: number) => void },
// U 会被推断为 string & number,即 never(因为参数位置是逆变的)