10

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)*]
Arc
  • 11,143
  • 4
  • 52
  • 75
  • You can't. Tuples have different types by index but fixed length, arrays don't let you define alternating types. It seems like your data structure is weird, why not use e.g. `[A, B][]`, an array of tuples? – jonrsharpe Apr 11 '20 at 10:43
  • Thanks. :-( It's for Elasticsearch bulk operations. – Arc Apr 11 '20 at 12:41
  • 1
    This is a good question. Wish there was a solution to this. – rpivovar Oct 15 '20 at 22:41
  • This question might be a duplicate of https://stackoverflow.com/questions/67315596/define-a-type-of-alternating-pairs/69787886#69787886 – captain-yossarian from Ukraine Nov 01 '21 at 21:05
  • @Arc could you please send me the link to elastic documentation with such requirement? – captain-yossarian from Ukraine Nov 18 '21 at 20:22
  • @captain-yossarianfromUkraine it's alternating actions/metadata and source data (the latter is optional, but in my case my bulk ops are alternating). And no, your linked question is a duplicate of this one, which is older :-) https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html – Arc Jun 08 '22 at 22:02

2 Answers2

9

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!

Matt Kantor
  • 1,704
  • 1
  • 19
  • 37
6

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.