Is there a way to require array elements with typescript so that I can do have
type E = keyof T; // Number of properties in T is unknown
let T
I'm going to define this:
type Arg = T extends any ? { arg: T } : never;
so that we can use Arg
(equivalent to {arg:"el1"}|{arg:"el2"}|{arg:"el3"}
) in what follows.
The best you can hope for here would be some generic helper function verifyArray()
which would enforce the restrictions that its argument is:
And it's going to be ugly.
There's no usable concrete type that will enforce this for unions containing more than about six elements. It is possible to use some illegally-recursive or legally-nonrecursive-but-tedious type definitions to take a union type like 0 | 1 | 2 | 3
and turn it into a union of all possible tuples that meet your criteria. That would produces something like
type AllTuples0123 = UnionToAllPossibleTuples<0 | 1 | 2 | 3>
which would be equivalent to
type AllTuples0123 =
| [0, 1, 2, 3] | [0, 1, 3, 2] | [0, 2, 1, 3] | [0, 2, 3, 1] | [0, 3, 1, 2] | [0, 3, 2, 1]
| [1, 0, 2, 3] | [1, 0, 3, 2] | [1, 2, 0, 3] | [1, 2, 3, 0] | [1, 3, 0, 2] | [1, 3, 2, 0]
| [2, 0, 1, 3] | [2, 0, 3, 1] | [2, 1, 0, 3] | [2, 1, 3, 0] | [2, 3, 0, 1] | [2, 3, 1, 0]
| [3, 0, 1, 2] | [3, 0, 2, 1] | [3, 1, 0, 2] | [3, 1, 2, 0] | [3, 2, 0, 1] | [3, 2, 1, 0]
But for an input union of n elements that would produce an output union of n! (that's n factorial) outputs, which grows very quickly in n. For your example "el1"|"el2"|"el3"
it would be fine:
type AllPossibleTuplesOfArgs = UnionToAllPossibleTuples>;
const okay: AllPossibleTuplesOfArgs =
[{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el3" }]; // okay
const bad1: AllPossibleTuplesOfArgs = [{ "arg": "el1" }, { "arg": "el2" }]; // error!
const bad2: AllPossibleTuplesOfArgs = // error!
[{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el2" }, { "arg": "el3" }];
const bad3: AllPossibleTuplesOfArgs =
[{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el8" }] // error!
but I assume you want something that doesn't crash your compiler when your object has seven or more properties in it. So let's give up on UnionToAllPossibleTuples
and any concrete type.
So what would verifyArray()
look like?
First let's make a type function called NoRepeats
which takes a tuple type T
and returns the same thing as T
if and only if T
has no repeated elements... otherwise it returns a modified tuple to which T
is not assignable. This will allow us to make the constraint T extends NoRepeats
to say "the tuple type T
has no repeated elements". Here's a way to do it:
type NoRepeats = { [M in keyof T]: { [N in keyof T]:
N extends M ? never : T[M] extends T[N] ? unknown : never
}[number] extends never ? T[M] : never }
So NoRepeats<[0,1,2]>
is [0,1,2]
, but NoRepeats<[0,1,1]>
is [0,never,never]
. Then verifyArray()
might be written as this:
const verifyArray = () => & readonly T[]>(
u: (U | [never]) & ([T] extends [U[number]] ? unknown : never)
) => u;
It takes a type T
to check against, and returns a new function which makes sure its argument has no repeats (from U extends NoRepeats
), is assignable to T[]
(from & readonly T
), and not missing any elements of T
(from & ([T] extends [U[number]] ? unknown : never)
). Yes it's ugly. Let's see if it works:
const verifyArgEArray = verifyArray>()
const okayGeneric = verifyArgEArray([{ "arg": "el3" }, { "arg": "el1" }, { "arg": "el2" }]); // okay
const bad1Generic = verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }]); // error
const bad2Generic = // error
verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el2" }, { "arg": "el3" }]);
const bad3Generic = // error
verifyArgEArray([{ "arg": "el1" }, { "arg": "el2" }, { "arg": "el8" }]);
So that works.
Both of these force you to fight with the type system. You could possibly make a builder class as in this answer which plays more nicely with the type system but involves even more runtime overhead, and is arguably only slightly less ugly.
Honestly I'd suggest trying to refactor your code not to require TypeScript to enforce this. The easiest thing is to require an object have these values as keys (e.g., just make a value of type T
or possibly Record
) and use that instead of (or before producing) an array. Oh well, hope this helps. Good luck!
Playground link to code