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)*]
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!
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With