Ensure a TS interface has all enum's values as keys

67 Views Asked by At

Assuming I have an enum:

enum Cars {
  Volvo = 'Volvo',
  Hyundai = 'Hyundai',
  Kia = 'Kia',
  Tesla = 'Tesla'
}

I want to define an interface or a type, which should have every enum value as a key (types for them may be different or same):

interface CarToFactory {
  [Cars.Volvo]: SwedishFactoryType,
  [Cars.Hyundai]: KoreanFactoryType,
  [Cars.Kia]: KoreanFactoryType,
  // <- Error: Missing Cars.Tesla
}

So adding a new car should throw an error unless you add a mapping to factory.

I tried to define an interface that extends Record<Cars, Factory>, but this doesn't give an error on missing property.

Also, the usage may not be obvious, but I have another interface with generic param, like:

interface CarPassport<TCar extends Cars> {
  company: TCar,
  manufacturer: CarToFactory[TCar],
  year: number,
}

This is not a question about defining JS objects, I need to solve it on TS side

2

There are 2 best solutions below

5
Igor Cantele On

Here I think the best approach is to split responsibilities:

  • You will have an object (e.g. carToFactory) responsible for mapping Cars to FactoryType, and it will enable you to have a correct inference and not a generic type FactoryType in CarPassport.manufacturer as you would have using a simple Record<Cars, FactoryType>.
  • You will have a type responsible to ensure that the entries of carToFactory match all the entries of Cars.
    The mapped type { [C in Cars]: typeof carToFactory[K] } ensure that all C are in carToFactory and the typeof carToFactory[K] part allows typescript to infer the right FactoryType
enum Cars {
  Volvo = 'Volvo',
  Hyundai = 'Hyundai',
  Tesla = "Tesla"
}

interface FactoryType {
  brand: string
}

class VolvoFactoryType implements FactoryType {
  brand = "Volvo"
}
class HyundaiFactoryType implements FactoryType {
  brand = "Hyundai"
}

const carToFactory = {
  [Cars.Volvo]: new VolvoFactoryType(),
  [Cars.Hyundai]: new HyundaiFactoryType(),
} as const


export type CarToFactory<K extends Cars> = { [C in Cars]: typeof carToFactory[K] } // <- Error: Missing Cars.Tesla
// Expects carToFactory keys to be all the values of Cars

interface CarPassport<TCar extends Cars> {
  company: TCar,
  manufacturer: CarToFactory<TCar>[TCar],
  year: number,
}

const carPassport: CarPassport<Cars.Hyundai> = {
  company: Cars.Hyundai,
  manufacturer: carToFactory[Cars.Hyundai],
  year: 2024
}
const shouldBeHyundaiFactoryType = carPassport.manufacturer
const carPassportInvalid: CarPassport<Cars> = {
  company: Cars.Hyundai,
  manufacturer: Cars,
  year: 2024
}

Here there is a playground with the code

0
Elias Salom On

It's an excellent question, here are some ideas that I think about it

enum Cars {
  Volvo = "Volvo",
  Hyundai = "Hyundai",
  Kia = "Kia",
  Tesla = "Tesla",
}

interface SwedishFactoryType {}
interface KoreanFactoryType {}
interface AmircanFactoryType {}

type FactoryType = SwedishFactoryType | KoreanFactoryType | AmircanFactoryType;

interface CarList extends Record<Cars, FactoryType> {}

const carList: CarList = {
  [Cars.Volvo]: {},
  [Cars.Hyundai]: {},
  [Cars.Kia]: {},
  [Cars.Tesla]: {},
};