TypeScript学习笔记 - 理解TypeScript中的infer
前言
先让我们考虑一个实际的例子:一个构造函数想要接受参数的是一个复杂的对象,但并没有将这个对象的类型导出。
class WebpackCompiler {
constructor(options: {
amd?: false | { [index: string]: any }
bail?: boolean
cache?:
| boolean
| {
maxGenerations?: number
type: "...
而在我们的代码中,我们可能会先将配置信息用一个变量存储,然后传递给构造函数。这时如果我们不小心把属性名拼错了,又没有类型提示,那就比较麻烦了。 而如果我们有办法把参数类型给提取出来,那我们就可以先对参数对象进行类型校验,避免一些错误。 这时候我们就需要用到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>
</>
);
}
如果我们稍微改下, 把传入的initialArg由number 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的类型,用来规范了返回值的类型。
总结
infer关键字是非常有用的,在react和vue3源码中也有很多应用之处,之前写TS比较随意,写了不少anyscript,所以最近在重学TypeScript,算是一个学习笔记吧,如有问题还请大家多多指正~