Mike Cluck
Mike Cluck

Reputation: 32511

Using an enum as a dictionary key

I'm trying to create a guaranteed lookup for a given enum. As in, there should be exactly one value in the lookup for every key of the enum. I want to guarantee this through the type system so that I won't forget to update the lookup if the enum expands. I tried this:

type EnumDictionary<T, U> = {
    [K in keyof T]: U;
};

enum Direction {
    Up,
    Down,
}

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

But I'm getting this weird error:

Type '{ [Direction.Up]: number; [Direction.Down]: number; }' is not assignable to type 'Direction'.

Which seems weird to me because it's saying that the type of lookup should be Direction instead of EnumDictionary<Direction, number>. I can confirm this by changing the lookup declaration to:

const lookup: EnumDictionary<Direction, number> = Direction.Up;

and there are no errors.

How can I create a lookup type for an enum that guarantees every value of the enum will lead to another value of a different type?

TypeScript version: 3.2.1

Upvotes: 53

Views: 42384

Answers (4)

Rasmond
Rasmond

Reputation: 512

With newer Typescript you can go with

Partial<Record<keyof typeof Direction, number>>

If you want all the keys to be required go with

Record<keyof typeof Direction, number>

Upvotes: 5

cefn
cefn

Reputation: 3331

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.

Upvotes: 3

rhigdon
rhigdon

Reputation: 1521

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,
}

Upvotes: 55

miensol
miensol

Reputation: 41648

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;
}

Upvotes: 60

Related Questions