In TypeScript (v4.5.4), I am trying to define an object type via an index signature. I want TypeScript to enforce certain sub-properties in the object to have matching types, but those types are allowed to vary between top-level properties.
In the below (non-working) example, I want all happy drivers to drive their favorite car type. A mismatch between a driver's favorite car type and the actual type of their car should cause a TypeScript compiler error.
type CarType = 'Minivan' | 'Sports car' | 'Sedan' ; // | 'Pickup truck' |, etc. Imagine this union type has many possible options, not just three.
type Car = {
carType: CarType
// A car probably has many additional properties, not just its type, but those are left out of this minimal example.
// The solution should be resistant to adding additional properties on `Car` (and the `Driver` type below).
};
type Driver = {
favoriteCarType: CarType
car: Car
};
/**
* Happy drivers drive their favorite car type.
*/
const happyDrivers: { [name: string]: Driver } = {
alice: {
favoriteCarType: 'Minivan',
car: {
carType: 'Minivan', // ✅ Alice drives her favorite type of car.
},
},
bob: {
favoriteCarType: 'Sports car',
car: {
carType: 'Sedan', /* ❌ Bob doesn't drive his favorite type of car!
This currently does not throw a compiler error because my types are too permissive, but I want it to. */
},
},
};
I've tried applying generics to the index signature and/or the Car
and/or Driver
type in all the ways I could think of, but I could not get the compiler to enforce the constraint that a driver's favoriteCarType
must exactly match their car
's carType
.
Can you help me out?
CodePudding user response:
What you are looking for is the following union:
type Driver = {
favoriteCarType: "Minivan";
car: {
carType: "Minivan";
};
} | {
favoriteCarType: "Sports car";
car: {
carType: "Sports car";
};
} | {
favoriteCarType: "Sedan";
car: {
carType: "Sedan";
};
}
You can generate this union if you make Car
generic (with the type of car being the type parameter) and we use a custom mapped type to create each constituent of the union (and then get a union indexing back into the resulting mapped type):
type Car<T extends CarType = CarType> = { carType: T };
type Driver = {
[P in CarType]: {
favoriteCarType: P
car: { carType: P }
}
}[CarType];
CodePudding user response:
Titian's answer put me on the right track by using mapped types. See the HappyDriver
type below.
type CarType = 'Minivan' | 'Sports car' | 'Sedan' ; // | 'Pickup truck' |, etc. Imagine this union type has many possible options, not just three.
type Car = {
carType: CarType
// A car probably has many additional properties, not just its type, but those are left out of this minimal example.
// The solution should be resistant to adding additional properties on `Car` (and the `Driver` type below).
};
/**
* A driver may drive a car of any type, not just their favorite.
*/
type Driver = {
favoriteCarType: CarType
car: Car
};
/**
* A happy driver drives their favorite type of car.
*/
type HappyDriver = {
[C in CarType]: {
[K in keyof Driver]: Driver[K] extends Car
? { [K2 in keyof Car]: Car[K2] extends CarType
? C
: Car[K2]
} : Driver[K] extends CarType
? C
: Driver[K]
}
}[CarType]
const happyDrivers: { [name: string]: HappyDriver } = {
alice: {
favoriteCarType: 'Minivan',
car: {
carType: 'Minivan', // ✅ Alice drives her favorite type of car.
},
},
bob: {
favoriteCarType: 'Sports car',
car: {
carType: 'Sedan', /*