Enforcing distinct array elements in TypeScript 4.1
Anders Hejlsberg has been on a hot streak recently, implementing several exciting features for the upcoming TypeScript 4.1 release. One of these is recursive conditional types, which I coincidentally found a use for in a personal project.
This project required a function that receives a range of keyword arguments that can appear in any order, but must be distinct from one another:
myFunction("three"); // valid
myFunction("two", "one"); // valid
myFunction("two", "one", "three"); // valid
myFunction("two", "two"); // invalid
myFunction("two", "two", "three"); // invalid
(This is not a design of my choosing, it’s a weird API compatibility issue…)
I didn’t get any useful search hits for variants of “enforce distinct array elements in TypeScript”, since most of the time people are looking for a runtime solution to this problem, so I thought I would document my solution.
type DistinctArray<T extends unknown[], U = T> = T extends []
? U
: T extends [head: infer Head, ...rest: infer Tail]
? Head extends Tail[number]
? never
: DistinctArray<Tail, U>
: never;
The DistinctArray
generic receives one type parameter for the array, and has a second type parameter that keeps track of the input array so that recursively instantiated types can resolve it.
- If the array has length zero, that counts as distinct, so resolve the input array.
- Otherwise, infer the head (first element) and tail (remaining elements) of the array.
- If the head is assignable to the union of the tail elements, then the array has duplicate elements, so resolve
never
. - Otherwise, recursively instantiate
DistinctArray
with the tail.
To constrain the arguments of a function, you could use it like this:
type MyFuncKeyword = "one" | "two" | "three";
type MyFuncArguments = [
MyFuncKeyword,
MyFuncKeyword?,
MyFuncKeyword?
];
function myFunction<T extends MyFuncArguments>(
...args: DistinctArray<T>
): void {};
myFunction("one", "two"); // OK
myFunction("one", "one"); // TS2345 error
Errors will have a vague “Argument of type ‘string’ is not assignable to parameter of type ‘never’” using the canonical never
type for failure modes. You could resolve a private string enum enum DistinctArguments { _ = "" }
instead to give users a hint, since they won’t be able to accidentally satisfy it with their own function declaration.
Let me know if you think of any other uses for this type!