Search code examples
typescripttypescript-typingstype-inference

Validate different types in the same array in TypeScript


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.


Solution

  • 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.

    Playground link to code