I’m trying to write a function that accepts a tuple of a union type but a specific member of the union should only appear once (or not at all), e.g. [A, B, A]
is valid but [A, B, A, B]
is not. Ideally I could just write something like [...A[], B, ...A[]] | A[]
but TypeScript doesn’t allow it.
While looking for possible solutions I found this answer to a slightly different but related problem: applying a complex constraint to a tuple type. The solution basically intersects the tuple with a generic type that either resolves to unknown
or never
. Through the use of recursion each member of the tuple can be validated.
Unfortunately my attempt doesn’t seem to work and I’m not sure why. Apparently I don’t understand the solution for the linked question too well. Example code below:
type A = number;
type B = string;
type EnsureOne<Tuple, T, Seen = false> =
Tuple extends [infer Head, ...infer Tail] ?
Head extends B ?
Seen extends true ?
{ ERROR: [`Only one value of type`, T, `is allowed`] } :
EnsureOne<Tail, T, true> :
EnsureOne<Tail, T, Seen> :
unknown;
type ExpectUnknown = EnsureOne<[A, B, A], B>;
// ^? unknown
type ExpectError = EnsureOne<[A, B, A, B], B>;
// ^? { ERROR: ["Only one value of type", string, "is allowed"] }
type Union = A | B;
declare function f<const T extends readonly Union[]>(t: T & EnsureOne<T, B>): void;
const expectError = f([0, '', '']); // no error :(
Playground
When I use EnsureOne
on its own it works as expected but in the intersection the constraint has no effect.