Reputation: 584
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
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
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
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
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