TypeScript infer 关键字

我们先来思考一个问题:假设你传入了一个函数的类型,那么能不能在条件类型中,精准地把这个函数的 “返回值类型” 给单独拽出来呢?

如果仅仅依靠 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 })的核心秘密。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号