TypeScript中compose函数为何出现参数类型推断失效问题?
自研函数式工具库开发过程中遇到compose函数参数类型推断异常问题,对照实现逻辑完全一致的Redux官方compose函数验证后,确认存在完全相同的问题。由于Redux普及度远高于个人自研库,下文统一以Redux的3参数compose类型声明为样本展开分析。
问题复现
支持传入3个函数的compose类型声明如下:
type Func<T extends any[], R> = (...a: T) => R function compose<A, B, T extends any[], R>( f1: (b: B) => R, f2: (a: A) => B, f3: Func<T, A> ): Func<T, R>
对应调用示例代码:
compose( // twoTimesNPlus100 被推断为unknown (twoTimesNPlus100) => `${twoTimesNPlus100} monkeys`, (nPlus100) => nPlus100 * 2, (n: number) => n + 100 )
检查类型推断结果可以发现:第二个函数的参数nPlus100可被正确推断为number类型,但第一个函数的参数twoTimesNPlus100类型推断失效,仅得到unknown类型。
该现象引出两个核心问题:
- 为什么同一份类型声明中,泛型
A可以被正确推导,泛型B却推导失败? - 是否存在可行的规避或修复方法?
问题根因
TypeScript对多参数函数的泛型上下文类型传导默认遵循从左到右的处理顺序,但compose的实际数据流是从右到左:最右侧函数是输入起点,其返回值会作为入参传给左侧相邻函数,依次向左传导,最终由最左侧函数返回结果。
在给出的3参数compose类型声明中,泛型依赖链为T(最右函数入参)→ A(最右函数返回值/中间函数入参)→ B(中间函数返回值/最左函数入参)→ R(最左函数返回值),但参数排列顺序为f1(最左函数,依赖B)→ f2(中间函数,依赖A、产出B)→ f3(最右函数,产出A)。TS从左到右处理参数时,首先处理f1,此时B没有任何推导来源,会被直接兜底为unknown;后续处理到f2、f3时,虽然可以通过f3显式标注的n: number推导出A为number,再通过f2的nPlus100 * 2逻辑推导出B应为number,但f1的参数类型已经在第一轮处理时被固定为unknown,TS不会反向回溯更新已经确定的类型,因此出现推断失效。
泛型A可以被正确推导的原因是:处理到最右侧的f3时,其入参有明确的number类型标注,直接可以确定A的类型,此时处理f2时A的类型已经就绪,因此f2的参数nPlus100可以拿到正确的number类型。
可行修复/规避方案
- 调整泛型声明顺序,适配从右到左的类型传导
将泛型参数的声明顺序从「从左到右对应函数顺序」改为「从左到右对应类型依赖顺序」,避免左侧泛型提前被兜底为unknown,修改后的3参数compose类型声明如下:
调整后TS会优先匹配最右侧的已知类型,沿依赖链从右向左传导,可正确推导出所有位置的函数参数类型。type Func<T extends any[], R> = (...a: T) => R function compose<R, B, A, T extends any[]>( f1: (b: B) => R, f2: (a: A) => B, f3: (...args: T) => A ): Func<T, R> - 多重重载覆盖全参数场景
这也是目前Redux官方采用的修复方案:针对1个函数、2个函数、3个函数……直到需要支持的最大函数入参个数,分别编写独立的重载类型声明,每个重载的泛型都按从右到左的依赖顺序排列,最后补充一个任意参数个数的兜底实现即可。 - 临时规避:手动标注类型
如果无法修改compose的类型声明,调用时给推断失败的参数手动标注对应类型即可绕过问题,示例:compose( (twoTimesNPlus100: number) => `${twoTimesNPlus100} monkeys`, (nPlus100) => nPlus100 * 2, (n: number) => n + 100 )
内容的提问来源于stack exchange,提问作者spender

