ShaneM
ShaneM

Reputation: 183

Type for array of subset of enum values (that warns if new enum member is not handled)

I have an enum:

enum Vehicles {
    CAR = 1,
    BOAT = 2,
    AIRPLANE = 3
}

and an array that contains a subset of that enum:

const vehiclesThatFly: VehiclesThatFly = [Vehicles.AIRPLANE]

How can I make a Typescript type, VehiclesThatFly, that will warn me when a new enum member is added to the enum so that I am forced to either 1) add that new member to my vehiclesThatFly array or 2. explicitly exclude it in the VehiclesThatFly type? In other words, if a new enum member is added, I do not want to have to remember to add it to vehiclesThatFly, I want TS to tell me that enum hasn't been handled.

I am imagining the VehiclesThatFly type would look something like this pseudocode: Exclude<(Vehicles enum values), Vehicles.CAR | Vehicles.BOAT>.

Is it possible to create a type like VehiclesThatFly or is there a better way to achieve my goal of having Typescript force me to explicitly handle new enum members?

Upvotes: 1

Views: 311

Answers (1)

Alex Wayne
Alex Wayne

Reputation: 187024

You could do something like:

type VehiclesSubset<T extends Record<Vehicles, boolean>> = {
  [K in keyof T]:
    T[K] extends true ? K : never
}[keyof T]

Here we have a generic type that accepts a type of vehicle/boolean pairs. It maps over these pairs, and if the value T[K] is true then uses that key K as the value, or if it is false then resolve to never. Then we index this mapped type by its own keys to get a union of the value types without the nevers.

Which would be used like:

type VehiclesThatFly = VehiclesSubset<{
  [Vehicles.CAR]: false
  [Vehicles.BOAT]: false
  [Vehicles.AIRPLANE]: true
}>

const airplaneCanFly: VehiclesThatFly = Vehicles.AIRPLANE
const carCantFly: VehiclesThatFly = Vehicles.CAR // error

And this constraint here:

Record<Vehicles, boolean>

Requires an entry for each key of Vehicles. So if you add an enum entry, you should get type errors:

enum Vehicles {
    CAR = 1,
    BOAT = 2,
    AIRPLANE = 3,
    MOTORCYCLE = 4,
}

type VehiclesThatFly = VehiclesSubset<{
  [Vehicles.CAR]: false
  [Vehicles.BOAT]: false
  [Vehicles.AIRPLANE]: true
}>
// Type '{ 1: false; 2: false; 3: true; }' does not satisfy the constraint 'Record<Vehicles, boolean>'.
//  Property '4' is missing in type '{ 1: false; 2: false; 3: true; }' but required in type 'Record<Vehicles, boolean>'.(2344)

See playground

Upvotes: 1

Related Questions