理解TypeScript中的infer关键字(23年更新)

15,316 阅读4分钟

前言

infer是在typescript 2.8中新增的关键字,几乎所有复杂的类型方法都有infer的身影。

infer

infer可以在extends的条件语句中推断待推断的类型。

例如我们可以使用infer来推断函数的返回值类型。

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

type func = () => number;
type variable = string;
type funcReturnType = MyReturnType<func>; // funcReturnType 类型为 number
type varReturnType = MyReturnType<variable>; // varReturnType 类型为 string

在这个例子中,infer R代表待推断的返回值类型,如果T是一个函数,则返回函数的返回值,否则返回T本身。

仅仅通过这一个例子,是很难看出infer是用来干什么的,还需要多看几个例子。

infer解包

infer的作用不止是推断返回值,还可以解包,假如想在获取数组里的元素类型,在不会infer之前我是这样做的。

type Ids = number[];
type Names = string[];

type Unpacked<T> = T extends Names ? string : T extends Ids ? number : T;

type idType = Unpacked<Ids>; // idType 类型为 number
type nameType = Unpacked<Names>; // nameType 类型为string

上次我写了20多行,就为了获取一堆各种不同类型的数组里的元素类型,然而如果使用infer来解包,会变得十分简单。

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

type idType = Unpacked<Ids>; // idType 类型为 number
type nameType = Unpacked<Names>; // nameType 类型为string

这里T extends (infer R)[] ? R : T的意思是,如果T是某个待推断类型的数组,则返回推断的类型,否则返回T

再比如,想要获取一个Promise<xxx>类型中的xxx类型。

type MyResponse = Promise<number[]>;
type Unpacked<T> = T extends Promise<infer R> ? R : T;

type resType = Unpacked<MyResponse>; // resType 类型为number[]

不仅如此,还可以使用递归来获取多层Promise的返回结果。

type MyResponse = Promise<Promise<Promise<number>>>;
type Unpacked<T> = T extends Promise<infer R> ? Unpacked<R> : T; // 递归

type resType = Unpacked<MyResponse>; // resType 类型为number

infer 推断模版字符串

模版字符串是v4.1新增的类型,这是官网的一个例子。

type World = "world";
type Greeting = `hello ${World}`; // "hello world"

我们可以写一个数字转字符串的类型方法。

type NumberToString<T extends number> = `${T}`;
type Value = NumberToString<5>; // "5"

同样,infer也可以推断模版字符串。

type PickValue<T> = T extends `${infer R}%` ? R : unknown;
type Value = PickValue<"50%"> // "50"

稍微复杂一点的,TrimLeft去除左侧空串。

type TrimLeft<T extends string> = T extends ` ${infer R}` ? TrimLeft<R> : T;
type Value = TrimLeft<"     value">; // "value"

如果再写一个TrimRight,组合起来,就能实现Trim方法,同时去除左右字符串。

infer推断联合类型

还是官方文档的例子。

type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;

type T10 = Foo<{ a: string; b: string }>; // T10类型为 string
type T11 = Foo<{ a: string; b: number }>; // T11类型为 string | number

同一个类型变量在推断的值有多种情况的时候会推断为联合类型,针对这个特性,很方便的可以将元组转为联合类型。

type ElementOf<T> = T extends (infer R)[] ? R : never;

type TTuple = [string, number];
type Union = ElementOf<TTuple>; // Union 类型为 string | number

infer 遍历数组

使用infer可以递归遍历数组,实现一个ReverseArray方法。

type ReverseArray<T extends unknown[]> = T extends [infer First, ...infer Rest]  
? [...ReverseArray<Rest>, First]  
: T  
type Value = ReverseArray<[1, 2, 3, 4]> // [4,3,2,1]

React中infer的使用

Reacttypescript源码中应该常常使用infer

就拿useReducer来举例子,如果我们这样使用useReducer

const reducer = (x: number) => x + 1;
const [state, dispatch] = useReducer(reducer, '');
// Argument of type "" is not assignable to parameter of type 'number'.

这里useReducer会报一个类型错误,说""不能赋值给number类型

那么React这里是如何通过reducer函数的类型来判断state的类型呢?

查看userReducer的定义,定义如下:

function useReducer<R extends Reducer<any, any>, I>(
  reducer: R,
  // ReducerState 推断类型
  initializerArg: I & ReducerState<R>,
  initializer: (arg: I & ReducerState<R>) => ReducerState<R>
): [ReducerState<R>, Dispatch<ReducerAction<R>>];

// infer推断
type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any>
  ? S
  : never;
// Reducer类型
type Reducer<S, A> = (prevState: S, action: A) => S;

一切明了了,使用了infer推断reducer函数里的state参数类型。

总结

infer是非常有用的,如果想要摆脱仅仅是在写带类型的javascript,高级特性一定要了解。

想要深入的练习infer,加强体操技巧,可以做一下type-chalenges

比如说实现上面我提到的Trim

题外话 分享一道比较复杂的练习题

原题就不贴出了,在这里可以看见 github

分享一下我的思路

  1. 首先先取得函数的名字,通过extends关键字可以判断是否是函数,是返回键名,不是返回never,最后使用映射类型[keyof T]的方式来获取键名的联合类型,因为never和任何类型组联合类型都会过滤掉never,所以自然排除了never
  2. 就用infer硬推

题解如下:

type EffectModuleFuncName = {
  [K in keyof EffectModule]: EffectModule[K] extends Function ? K : never;
}[keyof EffectModule];

type UnPackedPromise<T> = T extends Promise<infer P> ? P : T;

type EffectModuleFunc<T> = T extends (params: infer P) => infer U
  ? P extends Promise<infer R>
    ? (v: R) => UnPackedPromise<U>
    : P extends Action<infer X>
    ? (v: X) => UnPackedPromise<U>
    : never
  : never;

// 修改 Connect 的类型,让 connected 的类型变成预期的类型
type Connect = (
  module: EffectModule,
) => { [K in EffectModuleFuncName]: EffectModuleFunc<EffectModule[K]> };

也不知道自己写的对不对,总觉得怪怪的,可以讨论一下。


参考资料:

  1. TypeScript文档
  2. 深入理解TypeScript

最后,祝大家身体健康,工作顺利!

欢迎大家关注我的公众号~