Jermaine Cheng
Jermaine Cheng

Reputation: 43

Typing an array of generic inferred types

I'm trying to create the type of an array of objects. The types of key1 and key2 in each object must be be the same, but that type can be anything. Here's an example of a valid array:

[{ 
  key1: "hi",
  key2: "world"
},{
  key1: 1,
  key2: 2
},{
  key1: true,
  key2: false
}]

This is what I've come up with but it doesn't exactly work. I have a generic type to define the object in the array. When calling it to generate the array type, an error is raised.

type ArrayItem<T> = {
  key1: T,
  key2: T
}

// This raises an error Generic Type ArrayItem requires 1 type argument
type Array = ArrayItem<T>[]

What is the best way to type a nested object like this (with type inference support)?

Upvotes: 4

Views: 2492

Answers (2)

jcalz
jcalz

Reputation: 328503

If you don't have a finite list of possible types for T in ArrayItem<T>, there's no concrete type in TypeScript corresponding to Array<ArrayItem<T>>. To represent such a thing as a non-generic type would require something like existential types, which TypeScript doesn't directly support.

(If you do have a finite list, like ArrayItem<string> | ArrayItem<number> | ArrayItem<boolean>, then you can just use a union like in the other answer.)

The closest you can come to this in TypeScript is as a generic type, and the best you'll do in terms of inference and compiler warnings will be to represent this as something like a generic constraint.

One way to do this is to write a generic helper function asMyArray() accepting a tuple, and the compiler will check each element of the tuple to make sure it meets the constraint. One snag is that {key1: "hi", key2: 2} does meet the constraint if you allow things like string | number as T. To prevent the compiler from happily accepting all pairs of types, I will try to make it infer T from key1 only (see microsoft/TypeScript#14829 to see ways to prevent inferring from a particular inference site), and then just check that key2 matches that:

type NoInfer<T> = [T][T extends any ? 0 : 1]

const asMyArray = <T extends readonly any[]>(
    x: [...({ [K in keyof T]: { key1: T[K], key2: NoInfer<T[K]> } })]) =>
    x;

The generic type parameter T is a tuple corresponding to the key1 values for each element of the passed-in array. The passed-in array, x, is of a mapped tuple type. The & {} bit lowers the inference priority of key2. The [... ] bit just prompts the compiler to infer a tuple and not an array (where it wouldn't be able to tell the different elements apart), Lets test it out:

const myArray = asMyArray([{
    key1: "hi",
    key2: "world"
}, {
    key1: 1,
    key2: 2
}, {
    key1: true,
    key2: false
}])
// const asMyArray: <[string, number, boolean]>(...)

You can see that T is inferred as [string, number, boolean]. This succeeds, while the following, in which T is inferred the same way, fails:

const badArray = asMyArray([{
    key1: "hi", key2: 123 // error!
    // -------> ~~~~
    // number not assignable to string
}, {
    key1: 1, key2: "world" // error!
    // ----> ~~~~
    // string not assignable to number
}, {
    key1: true, key2: false
}]);

Looks like what you want.

Playground link to code

Upvotes: 5

Lu&#237;s Brito
Lu&#237;s Brito

Reputation: 1772

Dealing with arrays can be messy, even with generic typing. It depends deeply on how your items will be used after the array is initialized. Based on your snippets, I would start creating the interfaces for each "kind" of entry in the array, to get a strong typing for each distinct set of properties.

export interface FooItemType {
  key1: string,
  key2: string,
}


export interface BarItemType {
  key1: boolean,
  key2: boolean,
}

Then, you may create a new type that maps the interfaces you've defined before.

export type ItemType = BarItemType | FooItemType;

After that, you may declare it as simple ItemType array.

export myArr: ItemType[] = [{ 
  key1: "hi",
  key2: "world"
},{
  key1: 1,
  key2: 2
},{
  key1: true,
  key2: false
}]

Although this approach is strong typed, it may result in some manual casting after you get the myArray[i] item from the array. Sometimes we design the "typing" before thinking about its usage in the application, so the data structure has to be designed looking for the it as a whole.

Upvotes: 1

Related Questions