A type-level function that encodes a recursive relationship among items in a list

46 Views Asked by At

We have a generic class Cons that represents Lisp-like recursively defined linked lists.

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]
}

The list can contain heterogeneous items. Importantly, the type of the items is represented in the specific type of a Cons value. For example (with redundant type annotations for clarity):

const c1: Cons<1, Cons<2, Cons<3, undefined>>>
    = Cons.cons(3 as const).cons(2 as const).cons(1 as const)

I can encode some predicates on this family of types using a type-level function (i.e., a generic type that consumes mine and either returns it if the predicate is satisfied, or turns the type into never if the predicate is not satisfied).

For example, the recursive type-level function that asserts that all the elements in a list are of the same type:

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

We can test that AllSameType<Cons<1, Cons<1, Cons<1, undefined>>>> evaluates to its argument and AllSameType<Cons<3, Cons<2, Cons<1, undefined>>>> is never.

The following code also knows that the type of head is always the type variable 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)
    }
}

However, suppose I have a list of tuples Tuple<A, B>, and I want to encode the type of lists that are head-to-tail compatible, like dominoes: Cons<Tuple<A, B>, Cons<Tuple<B, C>, Cons<Tuple<C, D>, ...>>>:

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

Question: Is it possible to use AllCompatible in a (value-level) function without the type of the successively removed head elements decaying to Tuple<any, any>? How would I type the argument to such a function in TypeScript?

0

There are 0 best solutions below