3

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

 2 years ago
source link: https://segmentfault.com/a/1190000041482628
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

作者:小贼先生_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 类型应被推导为 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> = T extends number ? number : string

type T1 = LimitType<string>; // string
type 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 应符合 Action
type Dispatch<A extends Action> = (action: A) => A;

// 备注:此处 dispatch 的 js 实现只为示例说明,非 redux 中的真实实现
const dispatch: Dispatch<PayloadAction<number>> = (action) => action;

infer

条件类型中的类型推导。

// 推导函数的返回类型
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>; // string
type T2 = ArrayItemType<Date[]>; // Date
type 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;

// foo
interface 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;
  };
};

// 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<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...


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK