TypeScript学习笔记 - 理解TypeScript中的infer

709 阅读4分钟

TypeScript学习笔记 - 理解TypeScript中的infer

前言

先让我们考虑一个实际的例子:一个构造函数想要接受参数的是一个复杂的对象,但并没有将这个对象的类型导出。

class WebpackCompiler {
  constructor(options: {
    amd?: false | { [index: string]: any }
    bail?: boolean
    cache?:
      | boolean
      | {
          maxGenerations?: number
          type: "...

点击在Playground中查看

而在我们的代码中,我们可能会先将配置信息用一个变量存储,然后传递给构造函数。这时如果我们不小心把属性名拼错了,又没有类型提示,那就比较麻烦了。 而如果我们有办法把参数类型给提取出来,那我们就可以先对参数对象进行类型校验,避免一些错误。 这时候我们就需要用到infer关键词了。

infer关键字

infer算是TypeScript中的一大神器了。是在Typescript2.8中新增的关键字,这个关键字只能用在条件表达式的上下文中,是能够从其他类型中提取类型信息的重要工具。我们可以用它来解决上面遇到的问题。

infer的定义及使用

一句话给出infer的定义:infer只能用在条件语句中,表示在extends的条件语句中,以占位符形式出现用来修饰数据类型的关键字,被修饰的数据类型等到使用时会被推导出来。

一个典型的例子:

type inferType<T> = T extends (params: infer P) => string ? P : T

这个语句的意思是:如果 T 这个类型可以 分配给 (params: infer P) => string, 则会取出参数的类型 P, 否则,则取类型 T.

interface Coffee {
  id: number;
  name: string;
  brand: string;
  flavors: string[]
}
type CoffeeFnType = (params:  Coffee) => string
type inferType<T> = T extends (params: infer P) => string ? P : T

type param1 = inferType<CoffeeFnType>  // param1的类型即是 Coffee
type param2 = inferType<string>    // param2的类型即是string

这就是infer的作用。以占位符形式存在,当外部类型传进来后,会取出对应位置的数据的类型。

前面例子的解法

让我们再回头看最初的例子,我们如何利用infer去推导出构造函数参数的类型呢。我们可以先简化下问题。

class Coffee {
  constructor(flavor: string[]) {}
}

我们目标是拿到参数flavor的类型 string[],那我们该怎么做呢?结合前面讲到的infer的用法,我们很自然地可以写出:

type ConstructorArg<T> = T extends {new (args: infer P) : any} ? P : never
let flavor: ConstructorArg<typeof Coffee>

同样,我们可以通过infer关键字,拿到最开始的例子中的参数

let options: ConstructorArg<typeof WebpackCompiler>

这样就可以愉快地约束好传入参数的类型,避免一些不必要的错误。

infer关键字在React源码中的应用

日常开发中,useReducer是我们常用的一个Hooks API,直接拿官网的计数器例子来看(略微简化下)

function reducer(state: number, action):number {
  switch (action.type) {
    case 'increment':
      return {count: state + 1};
    case 'decrement':
      return {count: state - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, 1);
  return (
    <>
      Count: {state}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

在codesandbox中查看

如果我们稍微改下, 把传入的initialArgnumber 1改成string '1'

 -  const [state, dispatch] = useReducer(reducer, 1);
 +  const [state, dispatch] = useReducer(reducer, '1');
 
 // Argument of type 'string' is not assignable to parameter of type 'number'.ts(2769)

此时我们会得到一个ts报错Argument of type 'string' is not assignable to parameter of type 'number'.ts(2769) 那么React在这里是怎么知道state是number类型呢

看下useReducer的类型声明

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

type Dispatch<A> = (value: A) => void;
type Reducer<S, A> = (prevState: S, action: A) => S;
type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any> ? S : never; 
type ReducerAction<R extends Reducer<any, any>> = R extends Reducer<any, infer A> ? A : never;
  • 从传入的参数来看,reducer的类型是Reducer<any, any>, 返回的参数来看,state类型是ReducerState<R>

  • 然后看下Reducer<S, A>的定义,典型的reducer函数结构,接收参数state(S)和action(A),返回state(S)

  • 接着看ReducerState<R>,也就是根据 R extends Reducer<infer S, any>返回了state的类型: 即如果R可以分配给Reducer<infer S, any>,则返回类型S,也就是reducer函数第一个参数state的类型,否则返回never类型

type ReducerState<R extends Reducer<any, any>> = R extends Reducer<infer S, any> ? S : never;
  • 以计数器例子中的来看,其实State的类型就是这样获取的,通过infer关键字,最终我们计算得到的State类型就是number
type State = ReducerState<(s: number, action: {type: string}) => number>
  • 总结下,useReducer(reducer, initializerArg)中reducer函数先定义好自己的泛型,从而约束了state和action的类型,useReducer再通过infer关键字,获取到reducer函数中参数state的类型,用来规范了返回值的类型。

点击在Playground中查看

总结

infer关键字是非常有用的,在react和vue3源码中也有很多应用之处,之前写TS比较随意,写了不少anyscript,所以最近在重学TypeScript,算是一个学习笔记吧,如有问题还请大家多多指正~