TypeScript高阶函数中基于参数选字典回调及类型错误解决
TypeScript高阶函数类型匹配问题解决
现有一个函数marion,它接收回调对象和带有type属性的输入对象(type与回调对象的键匹配),能调用对应回调并传入输入对象。但将其封装为高阶函数higher后,调用返回的函数时出现TypeScript错误:输入对象无法赋值给never类型,原因是不同type的输入类型交集冲突被简化为never。
原代码示例
type Actor <K extends string, T> = (input: { type: K } & T) => void type Actors <K extends string, T> = { [Key in K]: Actor<K, T> } type TfromA<K extends string, A extends Actors<K, any>> = A extends Actors<K, infer T> ? T : never export function marion< K extends string, A extends Actors<K, TfromA<K, A>>, > ( actors: A, input: { type: K } & TfromA<K, A> ): void { const actor = actors[input.type] actor(input) } interface Alpha { type: 'alpha', count: number } interface Beta { type: 'beta', label: string } const alphaBeta = { alpha: (input: Alpha) => console.log(input.count), beta: (input: Beta) => console.log(input.label) } const alpha: Alpha = { type: 'alpha', count: 42 } marion(alphaBeta, alpha) // 无错误 function higher< K extends string, A extends Actors<K, TfromA<K, A>>, > ( actors: A ): (input: TfromA<K, A> & { type: K }) => void { return function (input: TfromA<K, A> & { type: K }): void { marion(actors, input) } } const lower = higher(alphaBeta) lower(alpha) // 错误:类型“Alpha”的参数不能赋给类型“never”的参数。 // 交集“Alpha & Beta & { type: string; }”被简化为“never”,因为某些组成部分的“type”属性类型冲突。ts(2345)
问题分析
原类型设计的核心问题在于Actors<K, T>强制所有回调共享同一个T,但实际场景中不同type对应的输入结构(如Alpha和Beta)是不同的。TfromA尝试从Actors中推断T,但会得到所有输入类型的交集,而Alpha和Beta的type属性分别为'alpha'和'beta',交集冲突导致推断结果为never,最终引发类型错误。
解决方案
需要重构类型定义,让每个回调对应各自的输入类型,而非统一的T:
重新定义基础类型:
Actor<I>:直接接收具体的输入类型I,无需拆分K和T。Actors<I>:回调对象的键对应输入类型的type属性值,每个键的回调接收对应的输入类型。
提取合法输入联合类型:
定义InputFromActors<A>,从回调对象A中提取所有合法的输入类型,形成联合类型。重构函数:
调整marion和higher的泛型约束,使用新的类型定义,确保TypeScript能正确推断每个输入对应的回调。
修改后的完整代码
// 单个回调类型,接收具体的输入类型I type Actor<I extends { type: string }> = (input: I) => void // 回调对象类型,键为输入的type值,对应回调接收该type的输入 type Actors<I extends { type: string }> = { [K in I['type']]: Actor<Extract<I, { type: K }>> } // 从回调对象中提取所有合法的输入类型联合 type InputFromActors<A extends Actors<any>> = A extends Actors<infer I> ? I : never export function marion<I extends { type: string }, A extends Actors<I>>( actors: A, input: I ): void { const actor = actors[input.type as keyof A] actor(input as Extract<I, { type: keyof A }>) } interface Alpha { type: 'alpha', count: number } interface Beta { type: 'beta', label: string } type AlphaBetaInput = Alpha | Beta const alphaBeta: Actors<AlphaBetaInput> = { alpha: (input: Alpha) => console.log(input.count), beta: (input: Beta) => console.log(input.label) } const alpha: Alpha = { type: 'alpha', count: 42 } const beta: Beta = { type: 'beta', label: 'test' } marion(alphaBeta, alpha) // 正常工作 marion(alphaBeta, beta) // 正常工作 // 高阶函数定义 function higher<I extends { type: string }, A extends Actors<I>>( actors: A ): (input: InputFromActors<A>) => void { return (input: InputFromActors<A>) => { marion(actors, input) } } const lower = higher(alphaBeta) lower(alpha) // 无错误 lower(beta) // 无错误
关键说明
Actor<I>直接绑定具体的输入类型,避免了拆分K和T带来的约束问题。Actors<I>通过Extract<I, { type: K }>为每个type键匹配对应的输入类型,确保回调与输入一一对应。InputFromActors<A>提取回调对象对应的所有输入联合类型,高阶函数返回的函数接收这个联合类型,TypeScript能自动进行类型窄化,确保输入与回调匹配。- 函数内部的类型断言是安全的,因为
input.type必然是actors的键之一,对应的输入类型也必然匹配回调的参数类型。
内容的提问来源于stack exchange,提问作者David Y. Stephenson
相关产品推荐
相关产品推荐

