TypeScript infer关键字在条件类型中的推导逻辑
在TypeScript的高级类型系统中,条件类型允许我们根据类型关系动态选择类型。infer关键字则是在此机制上实现类型推断的核心工具,它像一个“占位符”,让编译器在条件判断的某个位置自动推断并捕获一个类型。
掌握infer,意味着你能从复杂的类型结构中“提取”出你关心的部分,实现更灵活、更智能的类型编程。
1. 理解基本语法与定位
infer 只能在条件类型的 extends 子句中使用。它的作用是声明一个待推断的类型变量。
其基本结构如下:
type Extracted<T> = T extends SomeStructure<infer U> ? U : Fallback;
解读:T extends SomeStructure<infer U> 这句话的意思是:
- 判断
T是否是SomeStructure<V>的一个子类型。 - 如果是,那么
SomeStructure<V>这个“模具”与T匹配的过程中,那个未知的V会被推断出来,并绑定到我们声明的变量U上。 - 最终,类型
U就是条件为true时返回的类型。
简单来说,infer U 就是告诉TypeScript:“嘿,如果匹配成功,请把匹配到的那部分类型告诉我,我叫它 U。”
2. 核心使用场景与推导逻辑
我们将通过几个经典场景,一步步拆解 infer 是如何工作的。
场景一:提取函数返回类型
假设你想从一个函数类型中提取出它的返回值类型。这是最直观的用法。
// 定义一个工具类型,用于提取函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// 示例
type Fn1 = () => string;
type Result1 = ReturnType<Fn1>; // 推断结果为 string
type Fn2 = (x: number) => boolean[];
type Result2 = ReturnType<Fn2>; // 推断结果为 boolean[]
推导逻辑拆解:
- 匹配阶段:
T extends (...args: any[]) => infer R将Fn1(() => string) 视为(...args: any[]) => infer R的候选者。 - 推断阶段:函数签名中,
(...args: any[])匹配Fn1的参数部分(由于使用了any[],它能匹配任何参数列表)。=> infer R需要匹配Fn1的返回值部分。 - 绑定阶段:匹配成功,
Fn1的返回值string被捕获并赋予推断变量R。 - 选择阶段:条件成立,返回
R,即string。
内置的 ReturnType<T> 工具类型正是基于此原理实现的。
场景二:提取Promise内部的类型
从一个 Promise<SomeType> 中提取 SomeType。
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Test1 = UnwrapPromise<Promise<number>>; // number
type Test2 = UnwrapPromise<string>; // string (未匹配,返回T本身)
推导逻辑:Promise<infer U> 试图匹配 Promise<number>。infer U 占据了尖括号 <...> 内的位置,于是 number 被推断并绑定到 U。
场景三:提取元组的第一个元素类型
type Head<T extends any[]> = T extends [infer First, ...any[]] ? First : never;
type Tuple = [string, number, boolean];
type FirstElement = Head<Tuple>; // string
推导逻辑:[infer First, ...any[]] 是一个模式,它描述一个至少有一个元素的元组。infer First 匹配并捕获了元组的第一个元素类型。
场景四:深层嵌套提取(以提取对象嵌套属性类型为例)
假设你有一个嵌套对象类型,想安全地提取某个深层路径的类型。
type DeepGet<T, Path extends string[]> =
Path extends [infer First, ...infer Rest]
? First extends keyof T
? Rest extends [] // 如果Rest为空,说明到达终点
? T[First]
: DeepGet<T[First], Rest>
: undefined
: never;
interface Profile {
name: string;
address: {
city: string;
zip: number;
};
}
// 提取 `Profile['address']['city']` 的类型 `string`
type CityType = DeepGet<Profile, ['address', 'city']>;
推导逻辑(递归过程):
- 第一次调用
DeepGet<Profile, ['address', 'city']>:Path(['address', 'city']) 匹配[infer First, ...infer Rest],First被推断为'address',Rest被推断为['city']。 - 检查
'address'是Profile的key吗?是的。并且Rest(['city']) 不是空数组,所以递归调用DeepGet<Profile['address'], ['city']>,即DeepGet<{city: string; zip: number}, ['city']>。 - 第二次调用:
Path(['city']) 匹配模式,First推断为'city',Rest推断为[](空元组)。 - 检查
'city'是{city: string; zip: number}的key吗?是的。且Rest为空,到达终点,返回T[First],即{city: string; zip: number}['city'],结果为string。
3. 使用infer的关键点与限制
- 位置决定推断:
infer声明的变量类型由其在模式中所处的位置决定。放在函数返回值位置,推断返回类型;放在泛型参数位置,推断泛型参数。 - 协变与逆变:当同一个位置被多次
infer时(例如在联合类型中),推断出的类型是联合类型(协变)。type UnionInfer<T> = T extends { a: infer U; b: infer U } ? U : never; type Result = UnionInfer<{ a: string; b: number }>; // string | number (联合)在函数参数位置(逆变),推断出的类型是交叉类型。
type IntersectionInfer<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void; } ? U : never; type Result2 = IntersectionInfer<{ a: (x: string) => void; b: (x: number) => void; }>; // string & number (交叉) - 不可在“extends”子句外使用:
infer必须紧跟在extends关键字后面的类型结构中。 - 多层嵌套推断:可以在复杂的嵌套结构中使用多个
infer,如T extends Map<infer K, Set<infer V>>,同时推断键和值的类型。
4. 综合实战:构建一个Parameters工具类型
让我们自己动手实现一个提取函数参数类型的工具类型,加深理解。
// 错误示范:这样只能匹配固定参数数量的函数
// type MyParameters<T> = T extends (a: infer A) => any ? [A] : never;
// 正确实现:匹配任意参数列表
type MyParameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
// 测试
type Fn = (x: string, y: boolean) => void;
type Params = MyParameters<Fn>; // [x: string, y: boolean]
推导逻辑:
T extends (...args: infer P) => any:(...args: infer P)是一个 rest 参数模式,它能匹配任意数量和类型的参数列表。- 匹配
(x: string, y: boolean) => void时,整个参数列表[x: string, y: boolean]作为一个元组类型被捕获并赋予P。 - 条件为真,返回
P,即参数元组类型。
至此,你已经掌握了 infer 在条件类型中的核心推导逻辑。它的本质是一种模式匹配与类型捕获。通过在条件类型的 extends 子句中定义模式并放置 infer 占位符,你可以引导TypeScript编译器去解构复杂的类型,并将你需要的部分提取出来用于后续的类型计算。

暂无评论,快来抢沙发吧!