如何在值级函数中使用AllCompatible避免Tuple类型信息丢失?
泛型链表类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。下面的函数能正确识别所有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>, ...>>>),为此定义了AllCompatible:
type Tuple<F, S> = [F, S]; 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,避免每次取出的head元素类型退化为Tuple<any, any>?需要如何修改AllCompatible或为消费函数添加类型注解来保留精确类型信息?
解决方案
要保留AllCompatible链表中每个元素的精确类型,需要让AllCompatible不仅做类型校验,还要在递归过程中传递并保留节点间的类型关联,而非仅返回never或模糊的兼容类型。
修改AllCompatible类型
给AllCompatible添加泛型参数,显式跟踪链表节点的结束类型,从而在递归中传递类型约束:
type Tuple<F, S> = [F, S]; // 新增End泛型参数,跟踪链表最后一个Tuple的第二个元素类型 type AllCompatible<C, End = never> = C extends Cons<Tuple<infer Start, infer CurrentEnd>, infer Tail> ? Tail extends undefined ? End extends never ? C // 单节点链表直接返回原类型 : CurrentEnd extends End ? C : never // 验证单节点是否匹配指定End : Tail extends Cons<Tuple<CurrentEnd, infer NextEnd>, infer RestTail> ? Cons<Tuple<Start, CurrentEnd>, AllCompatible<Tail, End extends never ? NextEnd : End>> : never : never;
编写消费函数
基于修改后的AllCompatible,可以编写保留精确类型的消费函数:
function processCompatibleList< T extends Cons<Tuple<any, any>, Cons<any, any> | undefined> >(list: AllCompatible<T>) { let current: typeof list | undefined = list; while (current) { const [head, rest] = current.split(); // head类型会被精确推断为对应Tuple<X, Y>,而非Tuple<any, any> console.log(`当前节点:起始值 ${head[0]},结束值 ${head[1]}`); current = rest; } } // 测试示例:完全兼容的链表 const compatibleList = Cons.cons([3, 4] as const) .cons([2, 3] as const) .cons([1, 2] as const); processCompatibleList(compatibleList); // 正常执行,每个head类型精确
修改原理
修改后的AllCompatible通过End泛型参数在递归过程中传递类型约束,TypeScript可以沿着链表的递归结构,逐步推断出每个节点的Tuple类型,避免退化为any。同时它依然保留校验功能:若链表不满足首尾兼容,AllCompatible<T>会返回never,调用消费函数时会触发类型错误。
内容的提问来源于stack exchange,提问作者aas

