Define a type of alternating pairs

Viewed 357

From this question on How to define array with alternating types in TypeScript?, I've seen that it's possible to define a type of array where types must always go in an order such that the following are valid for Alternating<T, U>:

[]
[T]
[T, U]
[T, U, T]
[T, U, T, U]

Plus any length of items, provided that they are in an order such that elements of type U are always proceeded by an element of type T.

But the type definition is a little complex, and as such I'd want to avoid it - my use case is a little different, I would only want to have the following as valid within the type system:

[T, U]
[T, U, T, U]

Up to any length of array, provided that all values exist in a T, U pairing - no empty array, no 'dangling' T.

Is this possible? I had thought to try something like:

type AlternatingPairs<A, B> = [A, B, ...AlternatingPairs<A, B>];

Before I realised that you can't have circular references this way in typescript.

2 Answers

As per @KyleMit's request, this answer combines my comments into one formal block

If you must work with a "dynamic tuple", I highly suggest looking at the original answer on the linked question. It approaches the problem the other way around. Which, all things considered, is the only way to do what OP strictly asks for. You don't create the type of alternating pairs, you validate whether something falls under the type of alternating pairs.

I am, instead, going to talk about why creation of such a type is problematic, and offer an alternative representation of said "alternating pairs" type.

Why not?

The fundamental issue in making an "alternating pairs" type (or any dynamic "pair" type) is that a pair is strictly a tuple. In type theory, a tuple is a product type and thus memorizes each of its (possibly-)distinct resident types alongside their positional information.

The type [T, U] reads "A pair where T is the type of the first value, and U is the type of the second value". The type [T, U, T] is completely distinct from [T, U]. Even though typescript tuples are just javascript arrays, the difference is substantial in the type system. There simply is no way to have a dynamic tuple type, as a product type is exact and static.

What is an array, then?

An array is a homogenous collection. In type theory, an "array" (eh, it's more of a cons list but whatever) merely needs to hold the type of its residents overall, it doesn't care about their positions, and it doesn't care about its length (well, unless we're talking about dependent types - but that's a rabbit hole for another day). Notice how [1, "foo", True], when used as an array, has the type Array<number | string | boolean>. Its overall resident type is indeed number | string | boolean and it doesn't remember where exactly they are placed. You may add any type that is assignable to number | string | boolean to this array.

Another perspective

I want to take a moment to roll everything back. Going full abstract, OP's goal is a "dynamic, sequential collection of pairs". A dynamic, sequential collection is an array (not a static tuple). And a pair is.... a tuple. What if, instead of this-

[T, U] or [T, U, T, U] or [T, U, T, U, T, U] ad infinitum

We paired up the pairs to get-

[(T, U)] or [(T, U), (T, U)] or [(T, U), (T, U), (T, U)] ad infinitum

NOTE: That's not JS/TS syntax, I used parentheses there for clarity, tuples are, of course, represented with angle brackets instead ([[T, U], [T, U]...]).

Homogeneity - the one characteristic needed to have a dynamic sequential collection. That last type is just Array<[T, U]> or [T, U][] - an array of tuples - an array of pairs - a dynamic sequential collection of pairs.

That's the approach I sincerely recommend. It's much easier to understand, and probably, to maintain.

Irrelevant tidbits

Ok so the statement "arrays are a homogenous collection, in type theory" is a bit of a lie. But it's only a lie in good faith. See, heterogenous, well-typed arrays exist - in highly advanced type systems. They are.....difficult to tame though, and honestly irrelevant in the context of typescript. But you know, in case someone finds this stuff interesting, here's Haskell with generalized algebraic data types and type families.

Jeez those are some difficult names.

I admittedly didn't know about any of this when I made the comments here, that was quite a few months ago. I had too much fun with Haskell in the meantime :)

I know, that this implementation is not easy to understand and it might be not helpful for OP, but I leave it here since it might help other people:


type MAXIMUM_ALLOWED_BOUNDARY = 50

type Mapped<
    Tuple extends Array<unknown>,
    Result extends Array<unknown> = [],
    Count extends ReadonlyArray<number> = []
    > =
    (Count['length'] extends MAXIMUM_ALLOWED_BOUNDARY
        ? Result
        : (Tuple extends []
            ? []
            : (Result extends []
                ? Mapped<Tuple, Tuple, [...Count, 1]>
                : Mapped<Tuple, Result | [...Result, ...Tuple], [...Count, 1]>)
        )
    )



type Result = Mapped<[string, number]>

// 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24
type Test = Result['length']

PLayground

Mapped utility type just creates a union of all allowed states, like:

| [string, number]
| [stirng, number, string, number]
| [stirng, number, string, number, string, number]
| [string, number, ... ]

Every iteration I\m increasing Count array by 1. That's how I know when to stop recursive iteration.

You can create any type of allowed values. I mean, you can use triples instead of tuple. Example: Mapped<[string, number, number[]]>

Related question/answers you can find here and here ALso you can check my article regarding creating tuple with even length and validation it

UPDATE: Here you can find very similar question. I even think it is a duplicate

Related