TypeScript中如何在值级函数使用AllCompatible并避免类型退化?
背景:泛型链表类Cons
我们有一个泛型类Cons,用于实现类Lisp风格的递归链表:
class Cons<Head, Rest extends Cons<any, any> | undefined> { constructor(public head: Head, public rest: Rest) {} static cons: <Item>(item: Item) => Cons<Item, undefined> = (item) => new Cons(item, undefined) cons: <Item>(item: Item) => Cons<Item, this> = (item) => new Cons(item, this) split: () => [Head, Rest] = () => [this.head, this.rest] }
该链表支持异构元素,且元素类型会被精确体现在Cons实例的类型中,示例如下(添加冗余类型注解以增强可读性):
const c1: Cons<1, Cons<2, Cons<3, undefined>>> = Cons.cons(3 as const).cons(2 as const).cons(1 as const)
类型级函数:AllSameType
我们可以用类型级函数(即一种泛型类型,符合断言时返回原类型,否则返回never)对链表类型进行约束。比如下面这个递归类型级函数,用于断言链表所有元素类型相同:
type AllSameType<C> = C extends Cons<infer Head, infer Rest> ? Rest extends Cons<Head, infer _> ? Cons<Head, AllSameType<Rest>> : Rest extends undefined ? C : never : never
测试表明,AllSameType<Cons<1, Cons<1, Cons<1, undefined>>>>会返回原类型,而AllSameType<Cons<3, Cons<2, Cons<1, undefined>>>>则返回never。下面的reduce函数可以正确识别所有head的类型都是泛型变量Head:
function reduce<Head, Rest extends Cons<any, any>>(cons: AllSameType<Cons<Head, Rest>>) { let heads: Head[] = []; let [head, rest] = cons.split() heads.push(head) while (rest !== undefined) { [head, rest] = rest.split() heads.push(head) } }
多米诺兼容链表的类型级函数:AllCompatible
假设有一个由Tuple<A, B>元组组成的链表,我们需要定义类似多米诺骨牌、首尾元素兼容的链表类型:Cons<Tuple<A, B>, Cons<Tuple<B, C>, Cons<Tuple<C, D>, ...>>>,对应的类型级函数如下:
type Tuple<A, B> = [A, B]; type AllCompatible<C> = C extends Cons<infer Head, infer Tail> ? Head extends Tuple<infer A, infer B> ? Tail extends undefined ? Cons<Tuple<A, B>, undefined> : Tail extends Cons<Tuple<B, infer _1>, infer _2> ? Cons<Tuple<A, B>, AllCompatible<Tail>> : never : never : never
技术问题
能否在值级函数中使用AllCompatible类型,同时避免依次取出的头部元素类型退化为Tuple<any, any>?该如何为这类函数的参数进行TypeScript类型标注?
解答
可以实现该需求,核心是通过泛型参数捕获链表的完整兼容类型结构,而非直接将AllCompatible作为参数类型。具体实现如下:
1. 优化AllCompatible类型(可选,增强严谨性)
先明确Tuple定义,同时让AllCompatible更精准地递归约束链状兼容关系:
type Tuple<F, T> = [F, T]; type AllCompatible<C> = C extends Cons<Tuple<infer F, infer T>, infer Rest> ? Rest extends undefined ? C : Rest extends AllCompatible<Cons<Tuple<T, infer _>, infer __>> ? C : never : never;
2. 为值级函数标注泛型参数
通过泛型捕获链表的起始与后续兼容类型链,同时用AllCompatible做约束,让TypeScript保留每个节点的精确类型:
示例:遍历兼容链表并收集节点
function traverseCompatible<F, T, Rest extends Cons<any, any> | undefined>( cons: AllCompatible<Cons<Tuple<F, T>, Rest>> ): Tuple<any, any>[] { const nodes: Tuple<any, any>[] = []; let current: typeof cons = cons; while (true) { const [head, rest] = current.split(); // head类型会被精确推断为Tuple<F, T>,后续节点类型依次匹配 nodes.push(head); if (!rest) break; // 类型断言确保rest符合AllCompatible约束,避免类型退化 current = rest as AllCompatible<typeof rest>; } return nodes; }
进阶:捕获完整链类型以增强返回值精度
若需要让函数返回类型也体现链的兼容关系,可使用递归泛型捕获整个结构:
type CompatibleChain<F, T, Rest = undefined> = Rest extends CompatibleChain<T, infer NT, infer NRest> ? Cons<Tuple<F, T>, Rest> : Cons<Tuple<F, T>, undefined>; function traverseExact<F, T, Rest extends CompatibleChain<T, any> | undefined>( cons: CompatibleChain<F, T, Rest> ): Tuple<any, any>[] { const nodes: Tuple<any, any>[] = []; let current: typeof cons = cons; while (true) { const [head, rest] = current.split(); nodes.push(head); if (!rest) break; current = rest as CompatibleChain<T, any, typeof rest>; } return nodes; }
3. 测试验证
创建符合兼容要求的链表,函数可正确推断每个节点的精确类型:
// 创建多米诺兼容链表 const domino = Cons.cons([3, 4] as const) .cons([2, 3] as const) .cons([1, 2] as const); // 调用traverseCompatible,head类型不会退化为Tuple<any, any> traverseCompatible(domino); // 每个节点类型分别为Tuple<1,2>、Tuple<2,3>、Tuple<3,4>
内容的提问来源于stack exchange,提问作者aas

