CookieEater
CookieEater

Reputation: 2496

Interface typing with exhaustive enum check

I have a enum and I let each value of the enum as a key in my interface whose value is specific to the key.

Example:

enum Fruits {
  Apple = "Apple",
  Banana = "Banana",
}

// EDIT: These slicers can have completely different shapes.
interface AppleSlicer { foo: () => string }
interface BananaSlicer { bar: () => number }

interface FruitSlicers {
  [Fruits.Apple]: AppleSlicer,
  [Fruits.Banana]: BananSlicer,
}

This works well, but I want have similar code in several places and I want them to give me compile errors when there's a new entry in the enum. Currently, this doesn't do any exhaustive check, so it does not. Is it possible to achieve that with TypeScript?

Upvotes: 3

Views: 1472

Answers (3)

jsejcksn
jsejcksn

Reputation: 33749

You can use an identity function to constrain the input parameter type so that that it must include all of the keys in the string enum, and also all of the entries in the interface:

TS Playground

enum Fruit {
  Apple = 'Apple',
  Banana = 'Banana',
  Orange = 'Orange',
}

interface AppleSlicer { foo: () => string }
interface BananaSlicer { bar: () => number }

interface FruitSlicers {
  [Fruit.Apple]: AppleSlicer;
  [Fruit.Banana]: BananaSlicer;
}

function createSlicers <T extends Record<Fruit, unknown> & FruitSlicers>(value: T): T {
  return value;
}

const slicers1 = createSlicers({
  Apple: { foo: () => 'apple' },
  Banana: { bar: () => 1 },
}); // Error (2345)

const slicers2 = createSlicers({
  Apple: { foo: () => 'apple' },
  Banana: { bar: () => 1 },
  Orange: 'not in the interface, so can be any value',
}); // ok

Upvotes: 1

basarat
basarat

Reputation: 275947

For this you will need type compatibility checks. My recommended library for this is ts-expect: https://github.com/TypeStrong/ts-expect

Here is a complete sample (I've inlined expectType and TypeOf from ts-expect):

export const expectType = <Type>(value: Type): void => void 0;
export type TypeOf<Target, Value> = Exclude<Value, Target> extends never
  ? true
  : false;

type AppleSlicer = { apples: number }
type BananaSlicer = { bananas: number }

enum Fruits {
  Apple = "Apple",
  Banana = "Banana",
}

type FruitSlicersComplete = {
  [Fruits.Apple]: AppleSlicer,
  [Fruits.Banana]: BananaSlicer,
}
type FruitSlicersIncomplete = {
  [Fruits.Apple]: AppleSlicer,
  // [Fruits.Banana]: BananaSlicer,
}

type FruitSlicersExhaustive = {[key in Fruits]: any}

expectType<TypeOf<FruitSlicersExhaustive,FruitSlicersComplete>>(true); // Success ✅
expectType<TypeOf<FruitSlicersExhaustive,FruitSlicersIncomplete>>(true); // Error ❌

Upvotes: 3

millhouse
millhouse

Reputation: 10007

You probably want to make use of the Record utility type, which works great with enums.

Something like:

enum Fruits {
  Apple = 'Apple',
  Banana = 'Banana',
}

type Slicer = Function; // Quick example case

type FruitSlicers = Record<Fruits, Slicer>; // <- Ensures every enum value is a key

const fruitSlicers: FruitSlicers = {
  [Fruits.Apple]: () => {},
  [Fruits.Banana]: () => {},
};

Now if I add another Fruit:

enum Fruits {
  Apple = 'Apple',
  Banana = 'Banana',
  Cherry = 'Cherry',
}

I get:

Property 'Cherry' is missing in type '{ Apple: () => void; Banana: () => void; }' but required in type 'FruitSlicers'

Upvotes: 5

Related Questions