作者:小贼学生_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 = {    x: 1,    o: {      y: '2',    },  };  get(key) {    return this.data[key];  }}const c = new MyC();// 1. x 类型应被推导为 numberconst 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 不相熟也不会影响持续往下学习。

// footype FooModel = {  state: {    x: number;  };  reducers: {    add(      S: FooModel['state'],      A: {        payload: string;      },    ): FooModel['state'];  };};// bartype BarModel = {  state: {    y: string;  };  reducers: {    reset(      S: BarModel['state'],      A: {        payload: boolean;      },    ): BarModel['state'];  };};// modelstype 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> = T extends number ? number : stringtype T1 = LimitType<string>; // stringtype T2 = LimitType<number>; // 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<P = any> extends Action {  payload: P;  [extraProps: string]: any;}// 新增:Dispatch 类型,泛型 A 应合乎 Actiontype Dispatch<A extends Action> = (action: A) => A;// 备注:此处 dispatch 的 js 实现只为示例阐明,非 redux 中的实在实现const dispatch: Dispatch<PayloadAction<number>> = (action) => action;

infer

条件类型中的类型推导。

示例 1:

// 推导函数的返回类型type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;function fn(): number {  return 0;}type R = ReturnType<typeof fn>; // number

如果 T 能够调配给类型 (...args: any[]) => any,返回 R,否则返回类型 anyR 是在应用 ReturnType 时,依据传入或推导的 T 函数类型推导出函数返回值的类型。

示例 2:取出数组中的类型

type ArrayItemType<T> = T extends (infer U)[] ? U : T;type T1 = ArrayItemType<string>; // stringtype T2 = ArrayItemType<Date[]>; // Datetype T3 = ArrayItemType<number[]>; // 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> = T extends (...args: infer A) => any ? A : never;type FnArgs = Args<typeof fn>;

Fix: Q2 如何定义 get 办法

class MyC {  get<T extends keyof MyC['data']>(key: T): MyC['data'][T] {    return this.data[key];  }}

扩大:如果 get 反对「属性门路」的参数模式,如 const y = c.get('o.y'),TS 又当如何书写呢?

备注:此处只思考 data及深层构造均为 object 的数据格式,其余数据格式如数组等均未思考。

先实现 get 的传参类型:
思路:依据对象,自顶向下找出对象的所有门路,并返回所有门路的联结类型

class MyC {  get<P extends ObjectPropName<MyC['data']>>(path: P) {    // ... 省略 js 实现代码  }}{  x: number;  o: {    y: string  }}'x' | 'o' | 'o.y'type ObjectPropName<T, Path extends string = ''> = {  [K in keyof T]: K extends string    ? T[K] extends Record<string, any>      ? ObjectPath<Path, K> | ObjectPropName<T[K], ObjectPath<Path, K>>      : ObjectPath<Path, K>    : Path;}[keyof T];type ObjectPath<Pre extends string, Curr extends string> = `${Pre extends ''   ? Curr  : `${Pre}.`}${Curr}`;

再实现 get 办法的返回值类型:
思路:依据对象和门路,自顶向下逐层验证门路是否存在,存在则返回门路对应的值类型

class MyC {  get<P extends ObjectPropName<MyC['data']>>(path: P): ObjectPropType<MyC['data'], P> {    // ... 省略 js 实现代码  }}type ObjectPropType<T, Path extends string> = Path extends keyof T? T[Path]: Path extends `${infer K}.${infer R}`? K extends keyof T  ? ObjectPropType<T[K], R>  : unknown: unknown;

Fix: Q3 获取 dva 所有的 Actions 类型

type GenerateActions<Models extends Record<string, any>> = {  [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<AllModels>;

应用

// 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<S = any, A = any> = (state: S, action: A) => S;// foointerface FooState {  x: number;}type FooModel = {  state: FooState;  reducers: {    add: Reducer<      FooState,      {        payload: string;      }    >;  };};

Fix: Q3.2 抽取 Model

type Model<S = any, A = any> = {  state: S;  reducers: {    [reducerName: string]: (state: S, action: A) => S;  };};// foointerface FooState {  x: number;}interface FooModel extends Model {  state: FooState;  reducers: {    add: Reducer<      FooState,      {        payload: string;      }    >;  };}

Fix: Q3.3 无 payload ?

减少 WithoutNever,不为无 payloadaction 减少 payload 验证。

type GenerateActions<Models extends Record<string, any>> = {  [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<T> = Pick<  T,  {    [k in keyof T]: T[k] extends never ? never : k;  }[keyof T]>;

应用

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

Fix: Q3.4 非 payload ?

type GenerateActions<Models extends Record<string, any>> = {  [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<string, any>            ? {                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...

https://dev.to/tipsy_dev/adva...