6
头图
Author: Mr. Thief_ronffy

foreword

This article mainly explains the knowledge points such as extends , infer and template literal types of typescript. For each knowledge point, I will use them to solve some practical problems in daily development.
Finally, use these knowledge points to gradually solve the type problem when using .

illustrate:

  1. extends and infer are features introduced in TS 2.8.
  2. Template Literal Types is a feature introduced in TS 4.1.
  3. This article is not a typescript introductory document, and requires a certain TS foundation, such as TS basic types, interfaces, generics, etc.

Before formally speaking about the knowledge points, I will throw a few questions first, please think about each question carefully, and the following explanation will be slowly rolled out around these questions.

throw some questions

1. Get the parameter type of the function

function fn(a: number, b: string): string {
  return a + b;
}

// 期望值 [a: number, b: string]
type FnArgs = /* TODO */

2. How to define the get method

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. Get all Actions types of dva

is a data flow solution based on redux and redux-saga, which is a good data flow solution. I borrow 0621eebd09b927 in model here to learn how to better apply TS in practice. If you are not familiar with dva, it will not affect your continued learning.

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

Problem: Actions type 0621eebd09b985 from AllModels

// 期望
type Actions =
  | {
      type: 'foo/add';
      payload: string;
    }
  | {
      type: 'bar/reset';
      payload: boolean;
    };

Knowledge point

extends

extends has three main functions: type inheritance, conditional types, and generic constraints.

type inheritance

grammar:

interface I {}
class C {}
interface T extends I, C {}

Example:

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 is a conditional type used in conditional expressions.

grammar:

T extends U ? X : Y

If T conforms to the type range of U , return type X , otherwise return type Y .

Example:

type LimitType<T> = T extends number ? number : string

type T1 = LimitType<string>; // string
type T2 = LimitType<number>; // number

If T conforms to the type range of number , return type number , otherwise return type string .

generic constraints

extends can be used to constrain the scope and shape of generics.

Example:
Goal: TS verification of the passed parameters when calling dispatch method: type , payload and are mandatory attributes, and the payload type is number .

// 期望:ts 报错:缺少属性 "payload"
dispatch({
  type: 'add',
})

// 期望:ts 报错:缺少属性 "type"
dispatch({
  payload: 1
})

// 期望:ts 报错:不能将类型“string”分配给类型“number”。
dispatch({
  type: 'add',
  payload: '1'
})

// 期望:正确
dispatch({
  type: 'add',
  payload: 1
})

accomplish:

// 增加泛型 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 deduction in conditional types.

Example 1:

// 推导函数的返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

function fn(): number {
  return 0;
}

type R = ReturnType<typeof fn>; // number

Returns R if T is assignable to type (...args: any[]) => any , otherwise returns type any . R is when ReturnType is used, the type of the return value of the function is deduced according to the incoming or deduced T function type.

Example 2: Take out the type in the array

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

Template strings are marked with backticks (\`), and union types in the template string will be expanded and combined.

Example:

function request(api, options) {
  return fetch(api, options);
}

How to use TS to constrain api to be a string starting with https://abc.com ?

type Api = `${'http' | 'https'}://abc.com${string}`; // `http://abc.com${string}` | `https://abc.com${string}`
Author: Mr. Thief_ronffy

Solve the problem

Now, I believe you have mastered extends , infer and template literal types, let's solve the problems thrown at the beginning of the article one by one.

Fix: Q1 Get the parameter type of the function

You have learned ReturnType above, and know how to get the return value type of the function through extends and infer . Let's see how to get the parameter type of the function.

type Args<T> = T extends (...args: infer A) => any ? A : never;

type FnArgs = Args<typeof fn>;

Fix: Q2 How to define get method

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

Extension: If get supports the parameter form of "attribute path", such as const y = c.get('o.y') , how should TS be written?

Note : Only the data format of data and the deep structure of object is considered here, and other data formats such as arrays are not considered.

First realize the parameter type of get :
Idea: According to the object, find all paths of the object from top to bottom, and return the union type of all paths

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}`;

Then implement the return value type of the get method:
Idea: According to the object and path, verify whether the path exists layer by layer from top to bottom, and return the value type corresponding to the path if it exists.

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 Get all Actions types of dva

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>;

use

// 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,
};

Continue a series of questions:

3.1 Extract Reducer
3.2 Extract Model
3.3 No payload ?
3.4 Non- payload ?
3.5 Reducer not pass State ?

Fix: Q3.1 Extract 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 Extract 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 No payload?

Add WithoutNever , not add payload verification for action without 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]
>;

use

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

Fix: Q3.4 Non-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];

use

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

Can the legacy Q3.5 Reducer not pass State?

The answer is yes, there are many ideas for this question, one of which is: state and reducer are both on the defined model , after state model inject the type of 0621eebd09bffc into reducer ,
In this way, when defining model of reducer , there is no need to manually pass state .

This question is left for everyone to think about and practice, and will not be expanded here.

Summarize

extends , infer , Template Literal Types and other functions are very flexible and powerful,
I hope that based on this article, you can think more about how to apply them in practice, reduce bugs, and improve efficiency.

Reference article

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#conditional-types

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#template-literal-types

https://dev.to/tipsy_dev/advanced-typescript-reinventing-lodash-get-4fhe


小贼先生
800 声望35 粉丝

Dream is a shit, beat it first !