Search code examples
typescripttypescript-generics

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


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


Solution

  • It looks like you want a type-level satisfies operator, which checks against a type without widening to that type.


    If you were doing this with a value instead of a type, then satisfies would work exactly how you want:

    const carToFactory = {
      [Cars.Volvo]: 1,
      [Cars.Hyundai]: 2,
      [Cars.Kia]: 3,
      // [Cars.Tesla]: 4 // <-- uncomment this and your errors go away
    } satisfies Record<Cars, unknown>; // error!
    //~~~~~~~~~
    //  Property '[Cars.Tesla]' is missing in type 
    // '{ Volvo: number; Hyundai: number; Kia: number; }' 
    // but required in type 'Record<Cars, unknown>'
    

    There is no built-in thing that works this way at the type level, but you can define one yourself like this:

    type Satisfies<T, U extends T> = U;
    

    And use it like this:

    type CarToFactory = Satisfies<Record<Cars, unknown>, {
      [Cars.Volvo]: 1,
      [Cars.Hyundai]: 2,
      [Cars.Kia]: 3,
      // [Cars.Tesla]: 4 // <-- uncomment this and your errors go away
    }>;
    // error! Property '[Cars.Tesla]' is missing in type
    // { Volvo: 1; Hyundai: 2; Kia: 3; }' 
    // but required in type 'Record<Cars, unknown>'.
    

    Satisfies<T, U> checks T against U and reports an error if T is not assignable to U, but it evaluates to T without changing it. So if you forget a key as shown above, you get an error. The location of the error message isn't ideal (it underlines the entire type argument instead of a single piece of it) but the message is what you're looking for.

    Playground link to code