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!