首发于依赖注入
TypeScript 的 Infer 关键词

TypeScript 的 Infer 关键词

关注微信公众号“依赖注入”以获得更好阅读体验。

预计阅读完需要 15 min,点击这里获得更好的阅读体验。

ReturnType 是怎么实现的?

当你在使用 TypeScript 一段时间后,你可能会使用一些技巧来追求比较优雅的类型推断。如果你用过 TypeScript 内置工具类型 的话,你应该会好奇ReturnType是如何实现的?

type T0 = ReturnType<() => string>;  // string

实际上它并不神秘,你可以在这里找到所有内置工具类型的实现,如果你不想直面源码,这里还有一篇源码阅读笔记,可以找到 ReturnType 的实现如下:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

注意这里出现了 infer 关键词,官方对 infer 的解释是这样的:

Within the extends clause of a conditional type, it is now possible to have infer declarations that introduce a type variable to be inferred. Such inferred type variables may be referenced in the true branch of the conditional type. It is possible to have multiple infer locations for the same type variable.

翻译过来就是:

infer 关键词常在条件类型中和 extends 关键词一同出现,表示将要推断的类型,作为类型变量可以在三元表达式的 True 部分引用。而 ReturnType 正是使用这种方式提取到了函数的返回类型。

infer 的常见使用方式

1. 简单的类型提取

type Unpacked<T> =  T extends (infer U)[] ? U : T;

type T0 = Unpacked<string[]>; // string
type T1 = Unpacked<string>; // string

2. 嵌套的按顺序类型提取

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;

type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[]>;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked<Promise<string>>;  // string
type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

3. 使用多个 infer 变量进行类型提取

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

一道面试题

题目如下:

interface Logger {
  time: number;
  asyncLog:(msg: string) => Promise<string>
  syncLog:(msg: string) => number;
}

type Translate<T> = /** 你需要实现的部分 **/;

// 要求 Translate
//  1. 提取出为函数类型的属性,丢弃掉其它类型的属性
//  2. 将函数返回类型调整为形参类型(假定有且只有一个参数)

// 实现效果如下:
type T0 = Translate<Logger>;

// 等价于
type T0 = {
    // 其它属性被丢弃
    asyncLog: (arg: string) => string; // return 类型被调整为跟 arg 保持一致
    syncLog: (arg: string) => string; // return 类型被调整为跟 arg 保持一致
}

const result: T0 = {
    asyncLog(msg: string) { return msg }
};

解题思路:

强烈建议你此时打开编辑器做好准备同我一起解决这个问题,这大概会花费你 10 分钟时间。

1. 先实现一个类型可以提取出指定类型,用来筛选出所有为函数类型的属性

type FilterTypes<T, U> = {
    [Key in keyof T]: T[Key] extends U ? Key : never
};

// 看看阶段性成果
type T = FilterTypes<Logger, Function>;
// type T = {
//     time: never;
//     syncLog: "syncLog";
//     asyncLog: "asyncLog";
// }

2. 在 1 的基础上剔除 never,取出所有 key

type FilterKeys<T, U> = FilterTypes<T, U>[keyof T];

// 看看阶段性成果
type T = FilterKeys<Logger, Function>;
// type T = "syncLog" | "asyncLog"

3. 在 2 的基础上我们可以使用 Pick 提取出子类型

type SubType<T, U> = Pick<T, FilterKeys<T, U>>;

// 看看阶段性成果,此时我们已经成功提取出了所有类型为函数的属性,满足要求
type T = SubType<Logger, Function>;
// type T = {
//     syncLog: (msg: string) => number;
//     asyncLog: (msg: string) => Promise<string>;
// }

4. 在 3 的基础上我们再使用 infer 将函数的返回类型改为形参类型

// 将参数类型作为返回类型
type ArgAsReturn<T> = {
    [K in keyof T]: T[K] extends ((arg: infer U) => any) ? ((arg: U) => U): never;
}

// 我们最终得到了 Translate
type Translate = ArgAsReturn<SubType<Logger, Function>>;

// 看看最后效果,满足要求
type T = Translate<Logger>;

// type T0 = {
//     asyncLog: (arg: string) => string;
//     syncLog: (arg: string) => string;
// }


文中部分示例来自TypeScript 官网 - 高级类型

面试题灵感来自中国 LeetCode,原题太绕且有 Redux 倾向,因此做了简单改造,基本思路一致甚至更全面

编辑于 2021-10-17 00:26