Tagged union types are a great part of Typescript, indispensible when working with groups of related data types, the kind we are likely to receive in JSON form from some API. They allow us to generalize groups of those types and strong-type functions that can operate on any member of the group.
const catTag = `cat` as const;
const dogTag = `dog` as const;
type CatRecord = {
type: typeof catTag,
name: string,
huntsMice: boolean,
}
type DogRecord = {
type: typeof dogTag,
name: string,
lovesFetch: boolean
}
type AnimalRecord = CatRecord | DogRecord;
declare const cat: CatRecord
let a: AnimalRecord = cat; // all good :-)
TS playground ↗
This approach works fine in many situations, but its limits quickly become visible when we try modeling hierarchies of data.
const vanTag = `van` as const;
const ambulanceTag = `ambulance` as const;
type FuelType = 'gasoline' | 'diesel' | 'electric';
type VanRecord = {
type: typeof vanTag,
fuelType: FuelType
}
type AmbulanceRecord = Omit<VanRecord, 'type'> & {
type: typeof ambulanceTag,
medicalEquipment: string[]
}
type FuelStation = {
availableFuels: FuelType[]
}
function canRefuel(station: FuelStation, van: VanRecord) {
return station.availableFuels.some(x => x === van.fuelType);
}
declare const station: FuelStation;
declare const van: VanRecord;
declare const ambulance: AmbulanceRecord;
canRefuel(station, van); // fine
canRefuel(station, ambulance); // error!
TS playground ↗
So how can we both have tagged types and make sure that 'canRefuel' function accepts any type derived from 'Van'?