今天在看 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 } });
上面的AddAction和SubAction为了区分说明问题故意把 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中新加一个映射就行了。