JavaScript内核与高级编程之:`TypeScript` 的 `infer` 关键字:其在类型推断中的应用。

各位观众老爷,早上好!我是老码,今天给大家唠唠 TypeScript 里一个非常有趣,但也常常让人挠头的关键字:infer。 别看它只有五个字母,但它在类型推断的世界里可是个大杀器。 掌握了它,你的 TypeScript 类型体操水平,绝对能上一个新台阶。

今天咱们就以讲座的形式,深入浅出地剖析 infer 的用法,保证各位听完之后,都能把它玩得溜溜的。

一、啥是 infer

首先,我们得明白 infer 是干啥的。 简单来说,infer 是 TypeScript 中用于 类型推断 的一个关键字。 它的作用是:在条件类型中,允许我们声明一个类型变量,并让 TypeScript 自动推断出该变量的类型。

有点绕是吧? 没关系,咱们慢慢来。

想象一下,你是一位侦探,需要根据一些线索(类型条件)来推断出嫌疑人(类型变量)的身份。 infer 就相当于你手中的放大镜,帮助你从线索中提取出关键信息。

二、infer 的基本语法

infer 总是出现在条件类型中,它的基本语法是这样的:

type MyType<T> = T extends SomeType<infer U> ? TypeIfTrue : TypeIfFalse;

解释一下:

  • MyType<T>: 这是一个泛型类型,接受一个类型参数 T
  • T extends SomeType<infer U>: 这是一个条件类型。 它检查类型 T 是否可以赋值给 SomeType<infer U>
  • infer U: 这是 infer 的关键所在。 它声明了一个类型变量 U, TypeScript 会尝试从 SomeType<infer U> 中推断出 U 的类型。
  • TypeIfTrue: 如果 T 可以赋值给 SomeType<infer U>,那么 MyType<T> 的类型就是 TypeIfTrue。 在这里,你就可以使用 U 这个推断出来的类型了。
  • TypeIfFalse: 如果 T 不能赋值给 SomeType<infer U>,那么 MyType<T> 的类型就是 TypeIfFalse

三、infer 的常见应用场景

光说理论有点枯燥,咱们来看几个实际的例子,看看 infer 在哪些地方能大显身手。

1. 获取函数返回值类型

这是 infer 最常见的用法之一。 假设我们有一个函数类型,想知道它的返回值类型是什么,就可以用 infer 来推断。

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

function add(a: number, b: number): number {
  return a + b;
}

type AddReturnType = ReturnType<typeof add>; // AddReturnType 的类型是 number

解释一下:

  • ReturnType<T extends (...args: any) => any>: ReturnType 接收一个函数类型 T 作为参数。 T extends (...args: any) => any 确保 T 是一个函数类型。
  • T extends (...args: any) => infer R ? R : any: 这里使用了条件类型和 infer。 如果 T 是一个函数类型,那么 infer R 就会推断出返回值类型,并将其赋值给类型变量 R。 然后,ReturnType 的类型就是 R。 如果 T 不是一个函数类型,那么 ReturnType 的类型就是 any

2. 获取构造函数参数类型

类似于获取函数返回值类型,我们也可以用 infer 来获取构造函数的参数类型。

type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

class Person {
  constructor(name: string, age: number) {
    // ...
  }
}

type PersonConstructorParams = ConstructorParameters<typeof Person>; // PersonConstructorParams 的类型是 [string, number]

解释一下:

  • ConstructorParameters<T extends new (...args: any) => any>: ConstructorParameters 接收一个构造函数类型 T 作为参数。 T extends new (...args: any) => any 确保 T 是一个构造函数类型。
  • T extends new (...args: infer P) => any ? P : never: 如果 T 是一个构造函数类型,那么 infer P 就会推断出构造函数的参数类型,并将其赋值给类型变量 P。 然后,ConstructorParameters 的类型就是 P。 如果 T 不是一个构造函数类型,那么 ConstructorParameters 的类型就是 never

3. 获取数组元素的类型

infer 还可以用来获取数组元素的类型。

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

type NumberArray = number[];
type StringArray = string[];

type NumberElementType = ArrayElementType<NumberArray>; // NumberElementType 的类型是 number
type StringElementType = ArrayElementType<StringArray>; // StringElementType 的类型是 string

解释一下:

  • ArrayElementType<T>: ArrayElementType 接收一个类型 T 作为参数。
  • T extends (infer U)[] ? U : never: 如果 T 是一个数组类型,那么 infer U 就会推断出数组元素的类型,并将其赋值给类型变量 U。 然后,ArrayElementType 的类型就是 U。 如果 T 不是一个数组类型,那么 ArrayElementType 的类型就是 never

4. 获取 Promise 的 resolve 类型

有时候,我们需要知道一个 Promise resolve 的类型, infer 也能帮我们做到。

type PromiseResolveType<T> = T extends Promise<infer U> ? U : never;

async function fetchData(): Promise<string> {
  return "Data fetched!";
}

type FetchDataResolveType = PromiseResolveType<ReturnType<typeof fetchData>>; // FetchDataResolveType 的类型是 string

解释一下:

  • PromiseResolveType<T>: PromiseResolveType 接收一个类型 T 作为参数。
  • T extends Promise<infer U> ? U : never: 如果 T 是一个 Promise 类型,那么 infer U 就会推断出 Promise resolve 的类型,并将其赋值给类型变量 U。 然后,PromiseResolveType 的类型就是 U。 如果 T 不是一个 Promise 类型,那么 PromiseResolveType 的类型就是 never

5. 模式匹配 (Pattern Matching)

infer 还可以用于更复杂的模式匹配,比如提取元组类型中的特定元素。

type ExtractSecondElement<T extends any[]> = T extends [any, infer Second, ...any[]] ? Second : never;

type MyTuple = [number, string, boolean];

type SecondElementType = ExtractSecondElement<MyTuple>; // SecondElementType 的类型是 string

解释一下:

  • ExtractSecondElement<T extends any[]>: ExtractSecondElement 接收一个数组类型 T 作为参数。
  • T extends [any, infer Second, ...any[]] ? Second : never: 这里使用了模式匹配。 如果 T 是一个至少包含两个元素的元组类型,那么 infer Second 就会推断出第二个元素的类型,并将其赋值给类型变量 Second。 然后,ExtractSecondElement 的类型就是 Second。 如果 T 不是一个符合条件的元组类型,那么 ExtractSecondElement 的类型就是 never

四、infer 的高级用法与注意事项

上面的例子都是 infer 的基本用法。 实际上,infer 还可以玩出更多花样。

1. 多个 infer 的使用

在某些情况下,你可能需要在同一个条件类型中使用多个 infer。 例如,提取函数参数类型和返回值类型。

type FunctionTypeInfo<T extends (...args: any) => any> = T extends (...args: infer Args) => infer Return ? {
  args: Args;
  return: Return;
} : never;

function myFunc(a: number, b: string): boolean {
  return a > b.length;
}

type MyFuncInfo = FunctionTypeInfo<typeof myFunc>;
// MyFuncInfo 的类型是 { args: [number, string]; return: boolean; }

在这个例子中,我们同时使用了 infer Args 来推断参数类型,以及 infer Return 来推断返回值类型。

2. infer 与递归类型

infer 还可以与递归类型结合使用,构建更强大的类型操作。 例如,将一个嵌套的 Promise 类型展平到最内层的 resolve 类型。

type DeepPromiseResolve<T> = T extends Promise<infer U> ? DeepPromiseResolve<U> : T;

async function nestedPromise(): Promise<Promise<Promise<number>>> {
  return Promise.resolve(Promise.resolve(Promise.resolve(123)));
}

type NestedPromiseResolveType = DeepPromiseResolve<ReturnType<typeof nestedPromise>>; // NestedPromiseResolveType 的类型是 number

3. infer 的位置很重要

infer 出现的位置非常重要,它决定了 TypeScript 如何进行类型推断。 一定要确保 infer 出现在你想要推断的类型的位置。

4. 小心 any

如果 TypeScript 无法推断出 infer 的类型,它会默认使用 any。 这可能会导致类型安全问题,所以要尽量避免这种情况。 可以通过添加更严格的类型约束,或者提供默认类型来避免 any 的出现。

五、infer 的实际案例分析

为了让大家更好地理解 infer 的应用,我们来看一个更复杂的实际案例:实现一个 PickByType 工具类型,它可以从一个对象类型中选取指定类型的属性。

type PickByType<T, Value> = {
  [K in keyof T as T[K] extends Value ? K : never]: T[K];
};

interface Person {
  name: string;
  age: number;
  address: string;
  isEmployed: boolean;
}

type StringProps = PickByType<Person, string>; // StringProps 的类型是 { name: string; address: string; }

解释一下:

  • PickByType<T, Value>: PickByType 接收两个类型参数:T 是对象类型,Value 是要选取的属性类型。
  • [K in keyof T as T[K] extends Value ? K : never]: 这里使用了映射类型和条件类型。 它遍历 T 的所有属性键 K
  • T[K] extends Value ? K : never: 这是一个条件类型。 它检查属性 T[K] 的类型是否可以赋值给 Value。 如果可以,那么保留属性键 K;否则,将属性键 K 设置为 never,表示排除该属性。
  • T[K]: 如果属性键 K 被保留,那么 PickByType 的类型就是 T[K]

这个例子展示了 infer 在类型体操中的强大能力。 通过巧妙地使用 infer 和条件类型,我们可以实现各种复杂的类型操作。

六、总结

infer 是 TypeScript 中一个非常重要的关键字,它允许我们在条件类型中进行类型推断,从而实现更灵活、更强大的类型操作。 掌握 infer 的用法,可以帮助你编写更健壮、更可维护的 TypeScript 代码。

希望今天的讲座能帮助大家更好地理解 infer。 记住,熟能生巧,多练习、多实践,你就能真正掌握 infer 的精髓,成为 TypeScript 类型体操高手!

好了,今天的分享就到这里,感谢各位的观看,下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注