Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to define array with alternating types in TypeScript?

I would like to define an array type that allows different types depending on position, but in a repeating, alternating manner as found in some data structures.

Example:

[A, B, A, B, ...]
[A, B, C, A, B, C, ...]

Is this possible?

I know that I can define it for arrays with a fixed number of elements like above (without the ellipsis), and

(A | B)[]

would on the other hand allow any element to be either of type A or B.

I tried these:

[(A, B)...]
[...[A, B]]
[(A, B)*]
like image 968
Arc Avatar asked Apr 11 '20 10:04

Arc


2 Answers

I came up with something that "works", but it's kinda crazy:

type Alternating<T extends readonly any[], A, B> =
  T extends readonly [] ? T
  : T extends readonly [A] ? T
  : T extends readonly [A, B, ...infer T2]
    ? T2 extends Alternating<T2, A, B> ? T : never
  : never

This requires TypeScript 4.1+ because of the recursive conditional type.


Naive usage requires duplicating the value as a literal type for the T parameter, which is not ideal:

const x: Alternating<[1, 'a', 2], number, string> = [1, 'a', 2]

That seems strictly worse than just writing out [number, string, number] as the type. However with the help of a dummy function it's possible to avoid repetition:

function mustAlternate<T extends readonly any[], A, B>(
  _: Alternating<T, A, B>
): void {}

const x = [1, 'a', 2] as const
mustAlternate<typeof x, number, string>(x)

Here's a live demo with some test cases.


I wouldn't actually recommend relying on this in typical codebases (it's awkward to use and the error messages are terrible). I mostly just worked through it to see how far the type system could be stretched.

If anyone has suggestions for how to make it less wonky, I'm all ears!

like image 164
Matt Kantor Avatar answered Dec 02 '22 16:12

Matt Kantor


Alternative approach:

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, number[]]>

// 3 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48
type Test = Result['length']

/**
 * Ok
 */
const test1: Result = ['a', 42, [1]]
const test2: Result = ['a', 42, [1], 'b', 43, [2]]

/**
 * Fail
 */
const test3:Result = ['a'] // error

const fn = <T, U>(tuple: Mapped<[T, U]>) => tuple

fn([42, 'hello']) // ok
fn([42, 'hello','sdf']) // expected error

Playground

Mapped - ugly name but does the job :D. Creates a union of all allowed states of the tuple. Each allowed tuple state has a length which can be divided by 3: length%3===0 // true. You can define any tuple you want, with 4 values, 5 etc ...

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

like image 36
captain-yossarian Avatar answered Dec 02 '22 16:12

captain-yossarian