5

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.

KyleMit
  • 30,350
  • 66
  • 462
  • 664
OliverRadini
  • 6,238
  • 1
  • 21
  • 46
  • 2
    I think the original answer is the way to go for you. – Maciej Sikora Apr 29 '21 at 11:53
  • It's not possible to have a dynamic tuple type in typescript (what you're asking for). This problem is only approachable the other way around - i.e some constraint to verify a given type against this pattern. Similar to what the answer from your linked question does. – Chase Apr 29 '21 at 12:29
  • 1
    One fundamental thing that should be understood here - the distinction between arrays and tuples. In a type system, an array is homogenous, it's an array of type `T` (said `T` may be a union type) but the array is a dynamic collection of a homogenous type - with no positional info. Tuples encode positional info but do not allow dynamic lengths - since it's not possible to encode positions of an infinite/unknown number of elements. Hence there is no way to achieve a dynamic *tuple* type in the type system itself. – Chase Apr 29 '21 at 12:32
  • 1
    If possible, I'd suggest being explicit instead of hacky. You're asking for a **dynamic collection of pairs**. The type that fits that description is `(A, B)[]` - a list/array of pairs. In javascript that would look like- `[[A, B], [A1, B1], ....]` - where each element of the list is a tuple of type `[A, B]` – Chase Apr 29 '21 at 12:34
  • @Chase Thanks - I do agree that it would be better to have them grouped as pairs, the issue is that my use case will not allow that. It's good to know that this isn't possible in the type system, though. – OliverRadini Apr 29 '21 at 14:32
  • @Chase, your comments are super helpful (to me at least) - If you have time, you should post as an answer - I'd definitely upvote it :) – KyleMit Oct 31 '21 at 15:01
  • @KyleMit Sure, done. This was a while ago, but I think I managed to sum up my thoughts from April, while also adding some extra information. – Chase Oct 31 '21 at 17:11
  • Interesting responses - I'll leave this a while longer before accepting an answer to see what this develops – OliverRadini Oct 31 '21 at 17:59
  • 1
    @OliverRadini made an update – captain-yossarian from Ukraine Nov 01 '21 at 17:53

2 Answers2

3

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

3

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 :)

Chase
  • 5,315
  • 2
  • 15
  • 41