I have this interface:
enum CatEngine {
Electric = "ev",
Petrol = "petrol",
}
interface ElectricEngineDetails {
range: number
}
interface PetrolEngineDetails {
mpg: number
}
interface ICar {
engine: CarEngine;
details: ElectricEngineDetails | PetrolEngineDetails;
}
Is there a way to write a type guard that accepts an engine
and a details
, and returns the details
, narrowed down to the corresponding engine
type? I know it can be done in-place but I wish to extract it to a separate function.
CodePudding user response:
You could use a type
instead of an interface
for ICar
(or Car
since it's not an interface anymore):
type Car = { engine: CarEngine.Electric; details: ElectricEngineDetails } | { engine: CarEngine.Petrol; details: PetrolEngineDetails };
or you could create interfaces for the types:
type Car = IElectricCar | IPetrolCar;
interface IElectricCar {
engine: CarEngine.Electric;
details: ElectricEngineDetails;
}
interface IPetrolCar {
engine: CarEngine.Petrol;
details: PetrolEngineDetails;
}
CodePudding user response:
You can declare a generic interface, something like this:
type ICar<T extends CarEngine> = {
engine: T;
details: T extends CarEngine.Electric ? ElectricEngineDetails : PetrolEngineDetails;
}
This will compute the type for details
depending on the type of engine
. You can use it like this:
const ev: ICar<CarEngine.Electric> = {
engine: CarEngine.Electric,
details: {
range: 23,
}
}
Trying to use mpg
will result in an error, as TS knows that details
are of type ElectricEngineDetails
.
With this you can write up a type guard function that does the type narrowing:
const isElectric = (car: ICar<CarEngine.Electric> | ICar<CarEngine.Petrol>): car is ICar<CarEngine.Electric> => {
if (car.engine === CarEngine.Electric) {
return true;
}
return false;
}
Now you can use it like this:
function something (car: ICar<CarEngine.Petrol> | ICar<CarEngine.Electric>): void {
if (isElectric(car)) {
car.details.range;
} else {
car.details.mpg;
}
}
Here's a full example on playground.
CodePudding user response:
What you are looking for is a discriminated union
Basically, you need to define an union that is composed of all the possibilities, but all your interfaces must have a common and discriminant property that will allow Typescript to know how to eliminate types conditionally
You can test it here
enum CarEngine {
Electric = "ev",
Petrol = "petrol",
}
interface ElectricEngineDetails {
range: number
}
interface PetrolEngineDetails {
mpg: number
}
// Discriminant union, engine will be the discriminant property
type Car = IElectricCar | IPetrolCar;
interface IElectricCar {
// Electric car will have 'ev' as a type for engine
engine: CarEngine.Electric;
details: ElectricEngineDetails;
}
interface IPetrolCar {
// Petrol car will have 'petrol' as a type for engine
engine: CarEngine.Petrol;
details: PetrolEngineDetails;
}
declare const car: Car
// car types are discriminated based on the `engine` property
if (car.engine === CarEngine.Electric) {
car.details.range // ok
car.details.mpg // Doesn't exist
} else {
// Removed IElectricCar from the union because Typescript know car.engine can
// not be CarEngine.Electric anymore, so only IPetrolCar is available in the union
car.details.mpg // ok
car.details.range // Doesn't exist
}