一个很好用的Typescript泛型工具UnionRecord

258 阅读2分钟

今天在看 Typescript 4.6 的特性的时候,看到一个很好用的泛型工具,觉得非常好用,所以记录下。

interface TypeMap {
  number: number;
  string: string;
  boolean: boolean;
}

type UnionRecord<P extends keyof TypeMap> = {
  [K in P]: {
    kind: K;
    v: TypeMap[K];
  };
}[P];

UnionRecord的泛型P取值被限制为只能取"number""string""boolean",这三个取值可以任何组合。那么kind取值就只能是"number""string"或者"boolean"v的取值就为TypeMap[K],就是说如果kind的值是"number"的话,那么v的取值必须是一个number类型,否则就会报错。如下:

// 这是可以通过的
const a: UnionRecord<'number' | 'string'> = {
  kind: 'string',
  v: 'a',
};

// 这个就会报错
const b: UnionRecord<'number' | 'string'> = {
  //  ~~
  //  不能将类型“{ kind: "number"; v: string; }”分配给类型“UnionRecord<"string" | "number">”。
  //   属性“v”的类型不兼容。
  //     不能将类型“string”分配给类型“number”
  kind: 'number',
  v: 'b',
};
  • a.kind值为"string",那么a.v就必须是string类型的值,所以通过了。
  • b.kind值为"number",那么b.b就应该是number类型的值,但是这里却传入的是string类型的值,所以无法通过。

在扩展一下这个模式,我们在使用 redux 的时候,很常见的一个模式如下:

import { createStore, Action, AnyAction } from 'redux';

interface AddAction extends Action {
  type: 'add';
  payload: {
    add: number;
  };
}

interface SubAction extends Action {
  type: 'sub';
  payload: {
    sub: number;
  };
}

type Actions = AddAction | SubAction;

// 这里就直接写AnyAction了,不然createStore(counterReducer)的时候会报错,暂时不晓得咋搞
function counterReducer(state = { value: 0 }, action: AnyAction) {
  switch (action.type) {
    case 'add':
      return { value: state.value + action.payload.add };
    case 'sub':
      return { value: state.value - action.payload.sub };
    default:
      return state;
  }
}

let store = createStore(counterReducer);

store.subscribe(() => console.log(store.getState()));

store.dispatch<Actions>({ type: 'add', payload: { add: 1 } });

store.dispatch<Actions>({ type: 'sub', payload: { sub: 2 } });

上面的AddActionSubAction为了区分说明问题故意把 payload 的属性弄成不一样,但是实际开发中应该不会有自己给自己找事。上面的写法没啥问题,但是会定义很多的Action,每次新加一个Action,就需要修改至少两个地方:

  • 新加一个Action,如:OtherAction
  • type Actions = AddAction | SubAction;上面加一个联合类型,如:type Actions = AddAction | SubAction|OtherAction

所以我们通过今天的这个模式改写下:

import { createStore, AnyAction } from 'redux';

// 定义所有的Action的映射
interface ActionTypeMap {
  add: { add: number };
  sub: { sub: number };
}

type Actions<P extends keyof ActionTypeMap = keyof ActionTypeMap> = {
  [K in P]: {
    type: K;
    payload: ActionTypeMap[K];
  };
}[P];

function counterReducer(state = { value: 0 }, action: AnyAction) {
  switch (action.type) {
    case 'add':
      return { value: state.value + action.payload.add };
    case 'sub':
      return { value: state.value - action.payload.sub };
    default:
      return state;
  }
}

let store = createStore(counterReducer);

store.subscribe(() => console.log(store.getState()));

store.dispatch<Actions>({ type: 'add', payload: { add: 1 } });

store.dispatch<Actions>({ type: 'sub', payload: { sub: 2 } });

通过上面的改写后,我们新加一个Action就只需要在ActionTypeMap中新加一个映射就行了。