Danny Delott
Danny Delott

Reputation: 6998

Define a list of tuples that enforce shared values

I want to specify a type that enforces the last value in the previous tuple matches the first value in the next tuple, ie:

const contiguousIntervals: ThreeContiguousIntervals = [
  [0, 1],
  [1, 2],
  [2, 3],
];

I've gotten pretty close with the following definitions:

type IntervalMin<TEnd extends number> = [number, TEnd];

type IntervalMax<TStart extends number> = [TStart, number];

type Interval<TStart extends number, TEnd extends number> = [TStart, TEnd];

type ThreeContiguousIntervals<A extends number, B extends number> = [
  NumericIntervalMin<A>,
  NumericInterval<A, B>,
  NumericIntervalMax<B>
];

This works, but I have to pass the values in the generics signature:

// works!
const foo: ThreeContiguousIntervals<2, 3> = [
  [0, 2], [2, 3], [3, 4],
];

// breaks as expected
const bar: ThreeContiguousIntervals<2, 3> = [
  [0, 2], [3, 4], [4, 5],
           ^ throws error: "type '3' is not assignable to type '2'"
];

How do I go about getting TypeScript to infer the generic signature?

Upvotes: 0

Views: 54

Answers (1)

jcalz
jcalz

Reputation: 329168

The answer to your question as stated is to use a generic helper function and rely on type argument inference in that function, along with various tricks to get the inference not to be too wide (e.g., you don't want your bar to be inferred as ThreeContiguousIntervals<2|3, 4> but it could be).

But this is also similar to your other question, so I might as well give an similar answer to it whereby we support arbitrary length tuples instead of only tuples of length three...

You can use generic, mapped, and conditional types to represent the desired shape (a tuple of numeric pairs where the second element of each pair is the same type as the first element of the next pair) as a constraint on array types. Here's one way to do it:

// prepend a value to a tuple.  Cons<1, [2,3]> is [1,2,3]
type Cons<H, T extends any[]> = ((h: H, ...t: T) => any) extends
    ((...l: infer L) => any) ? L : never;

// verify that T is an array of numeric pairs where the last element of
// each pair is the same as the first element of the next pair
type VerifyContig<T> = T extends Array<any> ?
    { [K in keyof T]: [
        K extends '0' ? number :
        Cons<null, T> extends Record<K, [any, infer N]> ? N : never
        , number
    ] } : never;

// helper function to validate that values match the desired shape
const asContig = <N extends number, T extends [N, N][] | [[N, N]]>(
    contig: T & VerifyContig<T>
): T => contig;

asContig([]); // okay
asContig([[1, 2]]); // okay
asContig([[1, 2], [2, 3]]); // okay
asContig([[1, 2], [3, 4]]); // error!
//                 ~ <-- 3 is not assignable to 2
asContig([[1, 2], [2, 3], [3, 5], [5, 8], [8, 13]]); // okay
asContig([[1, 2], [2, 3], [3, 5], [5, 7], [8, 13]]); // error!
//            8 is not assignable to 7 --> ~

Looks good. Hope that helps; good luck!

Upvotes: 1

Related Questions