Using an enum as a dictionary key

You can do it as follows:

type EnumDictionary<T extends string | symbol | number, U> = {
    [K in T]: U;
};

enum Direction {
    Up,
    Down,
}

const a: EnumDictionary<Direction, number> = {
    [Direction.Up]: 1,
    [Direction.Down]: -1
};

I found it surprising until I realised that enums can be thought of as a specialised union type.

The other change is that enum types themselves effectively become a union of each enum member. While we haven’t discussed union types yet, all that you need to know is that with union enums, the type system is able to leverage the fact that it knows the exact set of values that exist in the enum itself.

The EnumDictionary defined this way is basically the built in Record type:

type Record<K extends string, T> = {
    [P in K]: T;
}

As of TypeScript 2.9, you can use the syntax { [P in K]: XXX }

So for the following enum

enum Direction {
    Up,
    Down,
}

If you want all of your Enum values to be required do this:

const directionDictAll: { [key in Direction] : number } = {
    [Direction.Up]: 1,
    [Direction.Down]: -1,
}

Or if you only want values in your enum but any amount of them you can add ? like this:

const directionDictPartial: { [key in Direction]? : number } = {
    [Direction.Up]: 1,
}

Given your use case I'm sharing a strategy I often use to solve this kind of problem although it's not strictly an Enum approach.

First I create a Readonly as const data structure - often an array is enough...

const SIMPLES = [
    "Love",
    "Hate",
    "Indifference",
    "JellyBabies"
  ] as const;

...and then I can simply use a mapped type over number to get the entries...

  type SimpleCase = (typeof SIMPLES)[number];

enter image description here

I like two things about the approach...

  • Runtime Access
  • Extensibility.

RUNTIME ACCESS

Often you can write procedures that ensure you don't miss anything rather than just seeing the issue as a redline or compile-time error...

const caseLabels = CASES.map((item) => item.toUpperCase());

EXTENSIBILITY

It's trivial to traverse whatever as const datastructure you come up with, meaning you're not dealing with the type system unnecessarily to define e.g. maps against typed definitions separately from types. However, you can buy into using it when you want, since the as const scheme gives the Typescript compiler access to 'inspect' the types you chose and you can use Mapped Types from there.

const COMPLEXES = {
  "Love":{letters:4},
  "Hate":"corrodes",
  "Indifference":() => console.log,
  "JellyBabies": [3,4,5]
} as const;

type Complex = keyof typeof COMPLEXES;
type ComplexRecord = typeof COMPLEXES[keyof typeof COMPLEXES];

show keys extracted as types show values extracted as types

Of course once your static record entries are addressable as a type, you can compose other data structures from them which will themselves enforce exhaustiveness against whatever your original data structure was.

  type ComplexProjection = {
    [K in Complex]:boolean;
  }

demonstrate exhaustiveness in projected type

For this reason I have never yet used an Enum, since I find there is enough power in the language already without them.

See this Typescript Playground to experiment with the approach demonstrated by the types shown above.