How MyType
should be defined to ensure that each item of my array is an array composed of item of the same type?
const array1: MyType = [["foo", "bar"], [42, 42], [true, false]]; // OK
const array2: MyType = [["foo", "bar"], [42, 42], [true, "false"]]; // Should throw a TS error
In my example I used string
, number
& boolean
, but it could be anything.
There is no specific type in TypeScript that works this way. Conceptually you want to have an existentially quantified generic type like
// don't write this, it doesn't work
type MyType = Array< <exists T>Array<T> >
to say that a MyType
is an array of arrays, where each element is of some array type. Well, that wouldn't quite work either, since [true, "false"]
is of the array type Array<true | "false">
, so presumably you'd need to try to guide inference away from the elements of each array after the first one, perhaps using the NoInfer
utility type like
// don't write this, it doesn't work
type MyType = Array< <exists T>[T, ...NoInfer<T>[]] >
but either way it doesn't work. TypeScript doesn't support existential types directly, and you can't say "some type". You can only say "for all types", by using universally quantified generics, which is all TypeScript (and most other languages with generics) supports.
That means you have to make MyType
generic. Like:
type MyType<T extends unknown[]> = { [I in keyof T]: readonly [T[I], ...NoInfer<T[I]>[]] }
So now you can write
const array1: MyType<[string, number, boolean]> =
[["foo", "bar"], [42, 42], [true, false, false]]; // okay
const array2: MyType<[string, number, boolean]> =
[["foo", "bar"], [42, 42], [true, "false"]]; // error
but that's redundant. You're forced to write out [string, number, boolean]
. It would be nice if TypeScript could infer that for you. Unfortunately that's not how generic types work. There's a feature request at microsoft/TypeScript#32794 which, if implemented, might mean you could write const array1: MyType<infer> = ⋯
, but for now it's not part of the language, so you need to work around this too.
TypeScript only infers generic type arguments when you call a generic function. So we could write a helper function that just returns its input at runtime, but gives you the inference you want, like this:
const myType = <T extends unknown[]>(t: MyType<T>) => t;
const array1 = myType([["foo", "bar"], [42, 42], [true, false, false]]); // okay
const array2 = myType([["foo", "bar"], [42, 42], [true, "false"]]); // error!
Looks good.
Note that the types of array1
and array2
are the same as before, but now you didn't have to annotate. Also note that const arr = myType([⋯])
isn't really much harder to write than const arr: MyType = [⋯]
, so even though you prefer to annotate instead of call a helper function, it's hopefully not too burdensome.