TypeScript中taskMap1与taskMap2的类型推断差异问题
为什么send1无法正确推断payload类型?
先看完整代码:
const Type = { CALL_REMIND: 'CALL_REMIND', CALL_SCHEDULED: 'CALL_SCHEDULED', } as const; type Type = keyof typeof Type; type TaskArgs = { [Type.CALL_REMIND]: { a: string }; [Type.CALL_SCHEDULED]: { a: number; b: number }; }; type TaskMap = { [TType in Type]: (args: TaskArgs[TType]) => void; }; const taskMap1 = { [Type.CALL_REMIND]: ({ a }: TaskArgs[typeof Type.CALL_REMIND]) => {}, [Type.CALL_SCHEDULED]: ({ a, b }: TaskArgs[typeof Type.CALL_SCHEDULED]) => {}, }; const taskMap2: TaskMap = { [Type.CALL_REMIND]: ({ a }: TaskArgs[typeof Type.CALL_REMIND]) => {}, [Type.CALL_SCHEDULED]: ({ a, b }: TaskArgs[typeof Type.CALL_SCHEDULED]) => {}, }; const send1 = <TType extends Type>(type: TType, payload: Parameters<typeof taskMap1[TType]>[0]) => { const res = taskMap1[type](payload); // 报错:类型“Parameters<typeof taskMap1[TType]>[0]”的参数不能赋给类型“never”的参数。 return res; } const send2 = <TType extends Type>(type: TType, payload: TaskArgs[TType]) => { const res = taskMap2[type](payload); return res; }
核心差异:taskMap1和taskMap2的类型本质不同
1. taskMap2是显式标注的映射类型
TaskMap是一个映射类型,它明确建立了Type成员与对应函数参数类型的绑定关系:对于每个TType,TaskMap[TType]的参数必然是TaskArgs[TType]。当你给taskMap2标注TaskMap类型后,TypeScript能理解这种泛型关联,所以send2中taskMap2[type](payload)可以完美匹配。
2. taskMap1是自动推断的具体对象类型
TypeScript对taskMap1的推断结果是一个静态的、非泛型的对象类型,结构如下:
const taskMap1: { readonly CALL_REMIND: ({ a }: { a: string; }) => void; readonly CALL_SCHEDULED: ({ a, b }: { a: number; b: number; }) => void; }
在send1中,泛型TType是Type的子类型,但TypeScript无法将TType和taskMap1的两个属性建立动态关联。它会认为taskMap1[TType]可能是两个函数中的任意一个,此时Parameters<typeof taskMap1[TType]>[0]会被计算为两个参数类型的交集——{ a: string } & { a: number; b: number },而这个交集的结果是never(因为a不可能同时是string和number),所以调用taskMap1[type](payload)时会报错。
修复send1的两种方案
方案一:给taskMap1显式标注TaskMap类型
直接把taskMap1的类型改成TaskMap,和taskMap2保持一致:
const taskMap1: TaskMap = { [Type.CALL_REMIND]: ({ a }) => {}, [Type.CALL_SCHEDULED]: ({ a, b }) => {}, };
这样TypeScript就能识别它的映射关系,send1可以正常工作。
方案二:基于taskMap1的类型推导泛型
如果不想修改taskMap1的类型,可以重新设计send1,让它直接基于taskMap1的结构推导泛型:
const createSender = <T extends Record<string, (...args: any[]) => void>>(map: T) => <K extends keyof T>(type: K, payload: Parameters<T[K]>[0]) => { return map[type](payload); }; // 使用 const send = createSender(taskMap1); send(Type.CALL_REMIND, { a: "hello" }); // 正确推断参数类型 send(Type.CALL_SCHEDULED, { a: 123, b: 456 }); // 正确推断参数类型
这种方式更灵活,不需要依赖预先定义的TaskArgs或TaskMap类型。
内容的提问来源于stack exchange,提问作者pvarouktsis
相关产品推荐
相关产品推荐

