如何让类型对齐序列构造器实现正确的类型推断?
嘿,这个问题我太有共鸣了——类型对齐序列(type-aligned list)的类型推断一直是TypeScript里的棘手问题,尤其是结合HKT的时候。你遇到的后续函数被推断成(x: unknown) => unknown的情况,本质上是因为TypeScript的泛型推断没办法自动链式关联每个步骤的输入输出类型,尤其是在变长参数的场景下。
你现在用的重载方式只能处理固定长度的参数,一旦参数变长,TypeScript就没办法把前一个函数的输出类型自动绑定到后一个函数的输入类型上。而且HKT的存在会让类型推断的复杂度再上一个台阶,因为TypeScript原生并不支持HKT,依赖hkt-core的代理类型会进一步限制推断能力。
下面给你两个可行的解决方案:
方案一:用链式调用替代变长参数(最可靠)
与其一次性传入所有函数,不如把构造过程拆成链式调用,每次添加一个元素时都基于前一个的输出类型进行推断。比如给你的TList加一个append方法:
class TList<L extends HKT.HKT2, In, Out> { // 假设你的TList内部有elements存储序列元素 private elements: HKT.Kind2<L, any, any>[]; // 其他已有方法... append<Next>(fn: HKT.Kind2<L, Out, Next>): TList<L, In, Next> { return fromArrayUnsafe([...this.elements, fn]) as TList<L, In, Next>; } }
然后使用的时候就变成:
const res = ta(proxy<FunctionHKT>(), proxy<number>()) .append(x => x + 1) .append(x => x.toString());
这种方式下,每次append都能明确知道当前函数的输入类型必须是前一个TList的输出类型,TypeScript的推断会非常顺畅,完全不会出现unknown的问题。
方案二:用递归元组约束实现变长参数推断
如果你坚持要用变长参数的方式,那可以利用TypeScript 4.0+支持的递归条件类型和可变元组来强制约束每个步骤的类型关联。首先定义几个辅助类型:
// 递归检查元组中的每个HKT是否类型对齐 type IsTypeAligned<L extends HKT.HKT2, PrevType, T extends any[]> = T extends [] ? true : T extends [HKT.Kind2<L, PrevType, infer NextType>, ...infer Rest] ? IsTypeAligned<L, NextType, Rest> : false; // 提取序列的最终输出类型 type GetSequenceEnd<L extends HKT.HKT2, StartType, T extends any[]> = T extends [] ? StartType : T extends [...any, HKT.Kind2<L, infer _, infer EndType>] ? EndType : StartType;
然后修改ta函数的泛型定义:
function ta< L extends HKT.HKT2, StartType, Steps extends HKT.Kind2<L, any, any>[] >( pl: Proxy<L>, pa: Proxy<StartType>, ...steps: IsTypeAligned<L, StartType, Steps> extends true ? Steps : never ): TList<L, StartType, GetSequenceEnd<L, StartType, Steps>> { // 这里需要类型断言,因为TypeScript无法完全解析递归条件类型的返回值 return fromArrayUnsafe(steps) as TList<L, StartType, GetSequenceEnd<L, StartType, Steps>>; }
这样修改后,当你传入不符合类型对齐的函数时,TypeScript会直接报错;而符合要求的函数会被正确推断类型。不过要注意,这种方式还是有一定局限性——如果序列太长,TypeScript可能会触发递归深度限制,而且类型推断的速度可能会变慢。
为什么原来的重载不行?
你之前写的重载是固定长度的(1个函数、2个函数等),但TypeScript的重载匹配是“贪婪”的,而且对于变长参数的泛型,它没办法自动把前一个参数的类型作为后一个参数的约束。比如第三个参数是(x: number) => number,但TypeScript不会自动把第四个参数的输入类型绑定成number,除非你显式用泛型约束把它们关联起来。
总的来说,链式调用的方式是最推荐的,因为它既符合函数式编程的风格,又能让TypeScript的类型推断发挥最佳效果。如果一定要用变长参数,递归元组约束是可行的,但确实要依赖TypeScript的高级类型特性,而且这也算是TypeScript类型系统的一个小局限——它天生不太擅长处理这种链式依赖的变长泛型推断。
内容来源于stack exchange

