Jeremie
Jeremie

Reputation: 584

How can TypeScript infer types from an array containing different types?

I am trying use an array containing multiple different types and have TypeScript infer the proper type for each element:

type House = {
    street: string;
    zipCode: number;
}

type Car = {
    make: string;
    model: string;
    year: number
}

const things: (House | Car)[] = [{
    street: '123 Fake Street',
    zipCode: 12345
}, {
    make: 'Tesla',
    model: 'Model X',
    year: 2022
},
{
    /* any other 'Car' or 'House' */
}]

things[1].year; // <---- Why doesn't TypeScript know it is a "Car" type and shows me an error here? 

Is there an easy way to help TypeScript with knowing the type without explicitly casting the array element?

Upvotes: 2

Views: 2099

Answers (3)

Mack
Mack

Reputation: 771

Since the array is typed as (House | Car)[], the compiler will let you add as many Houses or Cars to the array, in any position. So, during the compile step, it's unknown whether the item at any specific index is a House or a Car.

However, the compiler is smart enough to infer the type of an object if you check (during runtime) for the existence of a property that only exists on one of the possible types.

In your example, only Car has the make property, while House does not. So checking "make" in item confirms that item is a Car.

const item: House | Car = things[1];
if ("make" in item) {
    // the compiler is satisfied that item is a Car inside this block,
    // because only Cars have the "make" property
    item.year // <-- no error
}

Another useful pattern here is the discriminated union. You would declare your types like this:

type House = {
    kind: "house";
    street: string;
    zipCode: number;
}

type Car = {
    kind: "car";
    make: string;
    model: string;
    year: number
}

By checking the value of the kind property, the compiler is then satisfied which type of item you are looking at:

const item: House | Car = things[1];
if (item.kind === "car") {
   item.year // <-- no error
} else {
   item.street // <-- no error
}

Upvotes: 1

DeborahK
DeborahK

Reputation: 60626

You asked how TypeScript can infer types from an array containing different types. It can infer the types if you give it a little help and type check it first.

If you use a type guard to check the type as shown in this example, then TypeScript will correctly understand the type, and even provide the dropdown list of properties in the IDE.

function isSpecificType(item: House | Car): item is Car {
    return true;
}

if (isSpecificType(things[1])) {
  console.log(things[1].year)
} else  {
  console.log(things[1].street)
}

Based on the info from here: Switch for specific type in TypeScript

There is also some useful information on type guards here: https://www.typescriptlang.org/docs/handbook/advanced-types.html

Upvotes: 0

schpet
schpet

Reputation: 10666

bunch of different ways to do this, i'd recommend either a tuple type or a const assertion

Check out this TS playground link for an interactive example with the following code:

type House = {
    street: string;
    zipCode: number;
}

type Car = {
    make: string;
    model: string;
    year: number
}

const thingsA: [House, Car] = [{
    street: '123 Fake Street',
    zipCode: 12345
}, {
    make: 'Tesla',
    model: 'Model X',
    year: 2022
}]

let aHouse = thingsA[0]
let aCar = thingsA[1]
// @ts-expect-error
let aError = thingsA[2]


const thingsB = [{
    street: '123 Fake Street',
    zipCode: 12345
}, {
    make: 'Tesla',
    model: 'Model X',
    year: 2022
}] as const

let bHouse: House = thingsB[0]
let bCar: Car = thingsB[1]
// @ts-expect-error
let bError: Car = thingsB[0]

with real data, i find tagged unions aka discriminated unions (aka ADTs?) to be the best way to deal with this

playground link

type House = {
    _tag: "house";
    street: string;
    zipCode: number;
}

type Car = {
    _tag: "car",
    make: string;
    model: string;
    year: number
}

const thingsA: Array<House|Car> = [{
    _tag: "house",
    street: '123 Fake Street',
    zipCode: 12345
}, {
    _tag: "car",
    make: 'Tesla',
    model: 'Model X',
    year: 2022
}]


for (const thing of thingsA) {
  switch (thing._tag){
    case "house": {
      // now we know it's a house...
      console.log(thing.street)
      break
    }
    case "car": {
      // now we know it's a car...
      console.log(thing.model)
      break
    }
  }
}

Upvotes: 0

Related Questions