我们先来思考一个问题:假设你传入了一个函数的类型,那么能不能在条件类型中,精准地把这个函数的 “返回值类型” 给单独拽出来呢?
如果仅仅依靠 extends,我们只能做 “兼容性判断”(判断它是不是函数),却无法做 “局部类型提取”。
为了解决这个痛点,TypeScript 提供了一个专门用于在条件类型中 “捕获” 类型的关键字:infer。其中,infer 关键字只能在 “条件类型” 的条件判断语句中使用。
infer 关键字的作用非常巧妙:在做类型匹配时,让 TypeScript 帮你 “临时声明一个类型变量”,把匹配到的那部分未知类型存进去,然后你在后面的 true 分支里,就可以直接把这个变量拿出来用了。
提示: infer 的全称是 “inference(推断)”。
TypeScript infer 用于提取 “函数返回值”
举个企业开发中最真实的场景:我们调用了一个第三方库提供的工具函数,但源码里并没有单独导出这个函数的返回值 interface。而我们的业务组件偏偏又需要声明一个变量,来接收这个函数的返回值。
这时候,我们就可以利用 infer 关键字,强行把它的返回值类型 “吸” 出来。
示例 1:infer 提取函数的返回值类型
// 手写 MyReturnType
// 如果 T 兼容一个函数类型,那么就把它的返回值类型 “提取” 出来,存到变量 R 中
// 然后在问号后面,直接返回这个 R;否则返回 any
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// 假设这是一个第三方提供的复杂函数
function getUserInfo(id: number) {
return {
userId: id,
username: "Jack",
role: "Admin"
};
}
// 把 “函数值” 转成 “函数类型”
type UserFnType = typeof getUserInfo;
// 提取出返回值的对象结构
type UserInfo = MyReturnType<UserFnType>;
// 测试提取出来的类型
const user: UserInfo = {
userId: 1001,
username: "Lucy",
role: "Admin"
};
console.log(user.username);运行结果如下。
Lucy分析:
在 (...args: any[]) => infer R 这句硬核的代码中,我们并没有写死返回值是什么类型,而是放了一个 infer R 占位符。
TypeScript 编译器在推导时,发现 getUserInfo 的返回值是一个包含 userId、username 等字段的对象,于是它乖乖地把这个对象类型装进了变量 R 里面,然后原封不动地返回给我们。
TypeScript infer 用于提取 “数组元素的类型”
在 TypeScript 中,infer 除了可以处理函数,同样可以用于处理数组或元组。
假设我们有一个泛型 T,它可能是一个数组。如果是数组,我们希望剥离掉外层的 [],只拿里面元素的类型;如果不是数组,则原样返回。
示例 2:解构数组类型
// 如果 T 兼容数组类型,就把内部元素的类型提取到 ItemType 中
type ExtractArrayItem<T> = T extends readonly (infer ItemType)[] ? ItemType : T;
// 测试 1:传入 string 数组,完美提取出 string
type StrItem = ExtractArrayItem<string[]>;
const s: StrItem = "Hello TypeScript";
console.log(s);
// 测试 2:传入对象数组,完美提取出深层的对象结构
type ObjItem = ExtractArrayItem<{ id: number; title: string }[]>;
const o: ObjItem = {
id: 2026,
title: "绿叶网"
};
console.log(o.title);
// 测试 3:传入纯只读数组(如 as const 导出的元组),依然能完美提取!
type ReadonlyArrItem = ExtractArrayItem<readonly number[]>;
const numItem: ReadonlyArrItem = 100;
// 测试 4:传入普通数字,它不是数组,走 false 分支,原样返回
type Num = ExtractArrayItem<number>;运行结果如下。
Hello TypeScript
绿叶网分析:
在这个例子中,我们使用 Array<infer ItemType> 来进行模式匹配。
如果传入的泛型 T 确实是一个数组(比如 string[]),TypeScript 就会聪明地把数组内部元素的类型给 “吸” 出来,并存放到 ItemType 这个临时变量中。最后我们在 true 的分支里直接返回 ItemType,就完美实现了数组类型的解构提取。
反之,如果传入的 T 不是数组(比如测试 3 中的 number),条件判断失败,直接走 false 分支原样返回 T 本身。
TypeScript infer 用于解包 Promise
在现代前端项目(Vue 或 React)开发中,我们有大量的网络请求逻辑。通过 axios 或 fetch 拿到的返回值类型,通常是被 Promise 包裹的(例如 Promise<User>)。
如果要拿到纯净的 User 类型用于组件渲染,我们就必须把 Promise 的外壳给敲碎。
示例 3:提取 Promise 内部包裹的真实数据
// 如果 T 是 Promise 类型,就把内部包裹的真实数据类型提取到 U 中
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
// 模拟一个异步获取数据的函数返回值类型
type FetchUserResponse = Promise<{ name: string; age: number }>;
// 提取被包裹的真实数据结构
type RealUserData = UnwrapPromise<FetchUserResponse>;
// 此时 RealUserData 完美等价于 { name: string; age: number }
const data: RealUserData = {
name: "lvyenet",
age: 10
};
console.log("Promise 解包成功,获取到用户名:", data.name);运行结果如下。
Promise 解包成功,获取到用户名: lvyenet分析:
通过 Promise<infer U>,我们非常优雅地穿透了异步类型的屏障。实际上,TypeScript 官方提供的内置工具类型 Awaited<T>,其最底层的核心原理正是使用了这种基于 infer 的递归解包黑科技。
这里有小伙伴会问:“infer 可以提取函数的参数吗?”
答案是肯定的!infer 的位置非常自由,你把它放在哪里,它就提取哪里的类型。如果我们将 infer 放在函数的参数位置,就能轻松提取出参数的类型(这也是官方内置工具 Parameters<T> 的底层原理):
// 把 infer P 放在了参数的位置
type GetParams<T> = T extends (...args: infer P) => any ? P : never;
// 测试:提取一个接受两个参数的函数的参数类型
type MyFn = (name: string, age: number) => void;
type FnParams = GetParams<MyFn>;
// 此时 FnParams 被推断为一个元组类型:[name: string, age: number]TypeScript infer 用于提取 “字符串的局部内容”
自 TypeScript 4.1 引入模板字符串类型后,infer 的能力得到了史诗级的加强。我们不仅可以提取结构化的类型,甚至可以使用它去分割一个纯字符串,从而提取出我们想要的局部文本。
比如在绿叶网的业务开发中,我们需要写一个类型工具,专门用来提取网址(URL)前面的 “协议头”(即 http 或 https),如果传入的不是合法网址,则返回 unknown。
示例 4:利用 infer 提取字符串
// 使用占位符和 infer 提取特定位置的字符串
type ExtractProtocol<T> = T extends `${infer Protocol}://${string}` ? Protocol : "unknown";
// 测试 1:提取 https
type P1 = ExtractProtocol<"https://www.lvyenet.com">;
const protocol1: P1 = "https";
console.log(protocol1);
// 测试 2:提取 http
type P2 = ExtractProtocol<"http://localhost:3000">;
const protocol2: P2 = "http";
console.log(protocol2);
// 测试 3:传入非法格式,走 false 分支
type P3 = ExtractProtocol<"127.0.0.1">;
const protocol3: P3 = "unknown";
console.log(protocol3);运行结果如下。
https
http
unknown分析:
${infer Protocol}://${string} 这句代码非常硬核,在这个匹配规则中:
://:作为固定的分隔符。${string}:表示代表后续任意的字符串,我们不需要关心它是什么。${infer Protocol}:就是我们的 “吸星大法”。TypeScript 会自动把 :// 前面的那一截纯文本提取出来,塞进 Protocol 变量中。
通过这种方式,我们就在类型系统层面实现了一个 “字符串切割器”。这也是诸如 Vue Router 等顶级路由库底层推导动态路由参数(如把 /user/:id 推导为对象 { id: string })的核心秘密。
