Problem definition
Assume we have a React component C that accepts properties Props. Props have a field named edges. Edges are defined as a tuple of length 1-4 composed of string literals top, bottom, left, right.
Task: restrict the edges param to a tuple with no duplicates.
E.g.:
This should compile fine:
<C edges={['top', 'bottom']} />
while this should fail:
<C edges={['top', 'top']} />
What I have so far
// Our domain types
type Top = 'top';
type Bottom = 'bottom';
type Left = 'left';
type Right = 'right';
type Edge = Top | Bottom | Left | Right;
// A helper types that determines if a certain tuple contains duplicate values
type HasDuplicate<TUPLE> = TUPLE extends [infer FIRST, infer SECOND]
? FIRST extends SECOND
? SECOND extends FIRST
? true
: false
: false
: TUPLE extends [first: infer FIRST, ...rest: infer REST]
? Contains<FIRST, REST> extends true
? true
: HasDuplicate<REST>
: never;
// Just some helper type for convenience
type Contains<X, TUPLE> = TUPLE extends [infer A]
? X extends A
? A extends X
? true
: false
: false
: TUPLE extends [a: infer A, ...rest: infer REST]
? X extends A
? A extends X
? true
: Contains<X, REST>
: Contains<X, REST>
: never;
With the above I can already get this:
type DoesNotHaveDuplicates = HasDuplicate<[1, 2, 3]>; // === false
type DoesHaveDuplicates = HasDuplicate<[1, 0, 2, 1]>; // === true
Where I am stuck
Let’s say we have a component C:
// For simple testing purposes, case of a 3-value tuple
type MockType<ARG> = ARG extends [infer T1, infer T2, infer T3]
? HasDuplicate<[T1, T2, T3]> extends true
? never
: [T1, T2, T3]
: never;
interface Props<T> {
edges: MockType<T>;
}
function C<T extends Edge[]>(props: Props<T>) {
return null;
}
The above works but only like this:
// this compiles:
<C<[Top, Left, Right]> edges={['top', 'left', 'right']} />
// this does not (as expected):
<C<[Top, Left, Left]> edges={['top', 'left', 'left']} />
What I cannot figure out is how to get rid of the generics in component instantiation and make typescript deduce the types at compile time based on the value provided to the edges property.
>Solution :
I don’t really see the point of MockType. So let’s get rid of it.
Instead, use a conditional to check if HasDuplicate<T> is false. If it is, we can set the type of edges to be [...T].
interface Props<T extends Edge[]> {
edges: HasDuplicate<T> extends false ? [...T] : never;
}
The variadic tuple syntax is important here as it hints to the compiler that we want to infer T as a tuple. Otherwise T will be inferred to be an array with a union of Edge as its type.
// these compile
const a = (
<>
<C<[Top, Left, Right]> edges={['top', 'left', 'right']} />
<C edges={['top', 'left', 'right']} />
</>
)
// these do not compile
const b = (
<>
<C<[Top, Left, Left]> edges={['top', 'left', 'left']} />
<C edges={['top', 'left', 'left']} />
</>
)
One a site note: We can also simplify your HasDuplicate type a bit.
type HasDuplicate<TUPLE extends any[]> =
TUPLE extends [infer L, ...infer R]
? L extends R[number]
? true
: HasDuplicate<R>
: false