【干货】TypeScript|【干货】TypeScript 实战之 extends、infer 与 dva type

作者:小贼先生_ronffy
前言 本文主要讲解 typescript 的 extendsinfer 和 template literal types 等知识点,针对每个知识点,我将分别使用它们解决一些日常开发中的实际问题。
最后,活用这些知识点,渐进的解决使用 dva 时的类型问题。
说明:
  1. extendsinfer 是 TS 2.8 版本推出的特性。
  2. Template Literal Types 是 TS 4.1 版本推出的特性。
  3. 本文非 typescript 入门文档,需要有一定的 TS 基础,如 TS 基础类型、接口、泛型等。
在正式讲知识点之前,先抛出几个问题,请大家认真思考每个问题,接下来的讲解会围绕这些问题慢慢铺开。
抛几个问题 1. 获取函数的参数类型
function fn(a: number, b: string): string { return a + b; }// 期望值 [a: number, b: string] type FnArgs = /* TODO */

2. 如何定义 get 方法
class MyC { data = https://www.it610.com/article/{ x: 1, o: { y:'2', }, }; get(key) { return this.data[key]; } }const c = new MyC(); // 1. x 类型应被推导为 number const x = c.get('x'); // 2. y 类型应被推导为 string;z 不在 o 对象上,此处应 TS 报错 const { y, z } = c.get('o'); // 3. c.data 上不存在 z 属性,此处应 TS 报错 const z = c.get('z');

3. 获取 dva 所有的 Actions 类型
dva 是一个基于 redux 和 redux-saga 的数据流方案,是一个不错的数据流解决方案。此处借用 dva 中 model 来学习如何更好的将 TS 在实践中应用,如果对 dva 不熟悉也不会影响继续往下学习。
// foo type FooModel = { state: { x: number; }; reducers: { add( S: FooModel['state'], A: { payload: string; }, ): FooModel['state']; }; }; // bar type BarModel = { state: { y: string; }; reducers: { reset( S: BarModel['state'], A: { payload: boolean; }, ): BarModel['state']; }; }; // models type AllModels = { foo: FooModel; bar: BarModel; };

问题:根据 AllModels 推导出 Actions 类型
// 期望 type Actions = | { type: 'foo/add'; payload: string; } | { type: 'bar/reset'; payload: boolean; };

知识点 extends
extends 有三种主要的功能:类型继承、条件类型、泛型约束。
类型继承 语法:
interface I {} class C {} interface T extends I, C {}

示例:
interface Action { type: any; }interface PayloadAction extends Action { payload: any; [extraProps: string]: any; }// type 和 payload 是必传字段,其他字段都是可选字段 const action: PayloadAction = { type: 'add', payload: 1 }

条件类型(conditional-types) extends 用在条件表达式中是条件类型。
语法:
T extends U ? X : Y

如果 T 符合 U 的类型范围,返回类型 X,否则返回类型 Y
示例:
type LimitType = T extends number ? number : stringtype T1 = LimitType; // string type T2 = LimitType; // number

如果 T 符合 number 的类型范围,返回类型 number,否则返回类型 string
泛型约束 可以使用 extends 来约束泛型的范围和形状。
示例:
目标:调用 dispatch 方法时对传参进行 TS 验证:typepayload 是必传属性,payload 类型是 number
// 期望:ts 报错:缺少属性 "payload" dispatch({ type: 'add', })// 期望:ts 报错:缺少属性 "type" dispatch({ payload: 1 })// 期望:ts 报错:不能将类型“string”分配给类型“number”。 dispatch({ type: 'add', payload: '1' })// 期望:正确 dispatch({ type: 'add', payload: 1 })

实现:
// 增加泛型 P,使用 PayloadAction 时有能力对 payload 进行类型定义 interface PayloadAction extends Action { payload: P; [extraProps: string]: any; }// 新增:Dispatch 类型,泛型 A 应符合 Action type Dispatch = (action: A) => A; // 备注:此处 dispatch 的 js 实现只为示例说明,非 redux 中的真实实现 const dispatch: Dispatch> = (action) => action;

infer
条件类型中的类型推导。
示例 1:
// 推导函数的返回类型 type ReturnType = T extends (...args: any[]) => infer R ? R : any; function fn(): number { return 0; }type R = ReturnType; // number

如果 T 可以分配给类型 (...args: any[]) => any,返回 R,否则返回类型 anyR 是在使用 ReturnType 时,根据传入或推导的 T 函数类型推导出函数返回值的类型。
示例 2:取出数组中的类型
type ArrayItemType = T extends (infer U)[] ? U : T; type T1 = ArrayItemType; // string type T2 = ArrayItemType; // Date type T3 = ArrayItemType; // number

模版字符串类型(Template Literal Types)
模版字符串用反引号(\`)标识,模版字符串中的联合类型会被展开后排列组合。
示例:
function request(api, options) { return fetch(api, options); }

如何用 TS 约束 apihttps://abc.com 开头的字符串?
type Api = `${'http' | 'https'}://abc.com${string}`; // `http://abc.com${string}` | `https://abc.com${string}`

作者:小贼先生_ronffy
解决问题 现在,相信你已掌握了 extendsinfer 和 template literal types,接下来,让我们逐一解决文章开头抛出的问题。
Fix: Q1 获取函数的参数类型
上面已学习了 ReturnType,知道了如何通过 extendsinfer 获取函数的返回值类型,下面看看如何获取函数的参数类型。
type Args = T extends (...args: infer A) => any ? A : never; type FnArgs = Args;

Fix: Q2 如何定义 get 方法
class MyC { get(key: T): MyC['data'][T] { return this.data[key]; } }

扩展:如果 get 支持「属性路径」的参数形式,如 const y = c.get('o.y'),TS 又当如何书写呢?
备注:此处只考虑 data及深层结构均为 object 的数据格式,其他数据格式如数组等均未考虑。
先实现 get 的传参类型:
思路:根据对象,自顶向下找出对象的所有路径,并返回所有路径的联合类型
class MyC { get>(path: P) { // ... 省略 js 实现代码 } }{ x: number; o: { y: string } } 'x' | 'o' | 'o.y' type ObjectPropName = { [K in keyof T]: K extends string ? T[K] extends Record ? ObjectPath | ObjectPropName : ObjectPath: Path; }[keyof T]; type ObjectPath
= `${Pre extends '' ? Curr : `${Pre}.`}${Curr}`;

再实现 get 方法的返回值类型:
思路:根据对象和路径,自顶向下逐层验证路径是否存在,存在则返回路径对应的值类型
class MyC { get>(path: P): ObjectPropType { // ... 省略 js 实现代码 } }type ObjectPropType = Path extends keyof T ? T[Path] : Path extends `${infer K}.${infer R}` ? K extends keyof T ? ObjectPropType : unknown : unknown;

Fix: Q3 获取 dva 所有的 Actions 类型
type GenerateActions = { [ModelName in keyof Models]: Models[ModelName]['reducers'] extends never ? never : { [ReducerName in keyof Models[ModelName]['reducers']]: Models[ModelName]['reducers'][ReducerName] extends ( state: any, action: infer A, ) => any ? { type: `${string & ModelName}/${string & ReducerName}`; payload: A extends { payload: infer P } ? P : never; } : never; }[keyof Models[ModelName]['reducers']]; }[keyof Models]; type Actions = GenerateActions;

使用
// TS 报错:不能将类型“string”分配给类型“boolean” export const a: Actions = { type: 'bar/reset', payload: 'true', }; // TS 报错:不能将类型“"foo/add"”分配给类型“"bar/reset"”(此处 TS 根据 payload 为 boolean 反推的 type) export const b: Actions = { type: 'foo/add', payload: true, }; export const c: Actions = { type: 'foo/add', // TS 报错:“payload1”中不存在类型“{ type: "foo/add"; payload: string; }”。是否要写入 payload? payload1: true, }; // TS 报错:类型“"foo/add1"”不可分配给类型“"foo/add" | "bar/reset"” export const d: Actions = { type: 'foo/add1', payload1: true, };

继续一连串问: 3.1 抽取 Reducer
3.2 抽取 Model
3.3 无 payload
3.4 非 payload ?
3.5 Reducer 可以不传 State 吗?
Fix: Q3.1 抽取 Reducer
// 备注:此处只考虑 reducer 是函数的情况,dva 中的 reducer 还可能是数组,这种情况暂不考虑。 type Reducer = (state: S, action: A) => S; // foo interface FooState { x: number; } type FooModel = { state: FooState; reducers: { add: Reducer< FooState, { payload: string; } >; }; };

Fix: Q3.2 抽取 Model
type Model = { state: S; reducers: { [reducerName: string]: (state: S, action: A) => S; }; }; // foo interface FooState { x: number; } interface FooModel extends Model { state: FooState; reducers: { add: Reducer< FooState, { payload: string; } >; }; }

Fix: Q3.3 无 payload ? 增加 WithoutNever,不为无 payloadaction 增加 payload 验证。
type GenerateActions = { [ModelName in keyof Models]: Models[ModelName]['reducers'] extends never ? never : { [ReducerName in keyof Models[ModelName]['reducers']]: Models[ModelName]['reducers'][ReducerName] extends ( state: any, action: infer A, ) => any ? WithoutNever<{ type: `${string & ModelName}/${string & ReducerName}`; payload: A extends { payload: infer P } ? P : never; }> : never; }[keyof Models[ModelName]['reducers']]; }[keyof Models]; type WithoutNever = Pick< T, { [k in keyof T]: T[k] extends never ? never : k; }[keyof T] >;

使用
interface FooModel extends Model { reducers: { del: Reducer; }; } // TS 校验通过 const e: Actions = { type: 'foo/del', };

Fix: Q3.4 非 payload ?
type GenerateActions = { [ModelName in keyof Models]: Models[ModelName]['reducers'] extends never ? never : { [ReducerName in keyof Models[ModelName]['reducers']]: Models[ModelName]['reducers'][ReducerName] extends ( state: any, action: infer A, ) => any ? A extends Record ? { type: `${string & ModelName}/${string & ReducerName}`; } & { [K in keyof A]: A[K]; } : { type: `${string & ModelName}/${string & ReducerName}`; } : never; }[keyof Models[ModelName]['reducers']]; }[keyof Models];

使用
interface FooModel extends Model { state: FooState; reducers: { add: Reducer< FooState, { x: string; } >; }; } // TS 校验通过 const f: Actions = { type: 'foo/add', x: 'true', };

遗留 Q3.5 Reducer 可以不传 State 吗? 答案是肯定的,这个问题有多种思路,其中一种思路是:statereducer 都在定义的 model 上,拿到 model 后将 state 的类型注入给 reducer
这样在定义 modelreducer 就不需手动传 state 了。
这个问题留给大家思考和练习,此处不再展开了。
总结 extendsinfer 、 Template Literal Types 等功能非常灵活、强大,
希望大家能够在本文的基础上,更多的思考如何将它们运用到实践中,减少 BUG,提升效率。
参考文章 https://www.typescriptlang.or...
https://www.typescriptlang.or...
https://www.typescriptlang.or...
【【干货】TypeScript|【干货】TypeScript 实战之 extends、infer 与 dva type】https://dev.to/tipsy_dev/adva...

    推荐阅读