48

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

Mike Cluck
  • 31,869
  • 13
  • 80
  • 91
  • **See Also**: [Use Enum as restricted key type in Typescript](https://stackoverflow.com/q/44243060/1366033) – KyleMit Apr 06 '21 at 23:16

4 Answers4

52

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;
}
miensol
  • 39,733
  • 7
  • 116
  • 112
  • Ahhh, that makes sense now. I've been banging my head on this problem for a while now. Thank you! – Mike Cluck Feb 05 '19 at 20:33
  • 1
    Nice! I'd also suggest to use string values if possible, otherwise you'll get weird error message like "property 2 is missing in ..." once new enum members will be added – Aleksey L. Feb 05 '19 at 20:35
  • 1
    Observation: You MUST use all the enum values, which makes it rather un-dictionary'ish ;) – Torben Koch Pløen Jun 28 '19 at 08:29
  • 3
    @TorbenRahbekKoch I declare it this way: type Dictionary = Partial>; – Shalom Peles Aug 19 '19 at 00:12
  • 1
    Is there any way to make it so the dictionary requires all keys to be from the enum without requiring the dictionary to have an entry for all of the enum values? I get the error 'is missing the following ... ' with each enum value. – Fortytwo Oct 02 '19 at 16:18
  • 1
    To answer my own question here is a link to an answer I found shortly after finally deciding it was worth asking. https://stackoverflow.com/a/52700831/1034691 – Fortytwo Oct 02 '19 at 16:21
  • Thanks I'm using this but struggled a bit when using "for (let key in enumDictionary)..." because in that case "key" is actually a string of the index and not the actual enum value, and due to duck typing it was sometimes doing what I expected but not always. So I use parseInt(key) to get the true enum index number. – Etherman May 13 '20 at 16:11
51

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,
}
KyleMit
  • 30,350
  • 66
  • 462
  • 664
rhigdon
  • 1,483
  • 1
  • 15
  • 20
2

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.

cefn
  • 2,895
  • 19
  • 28
2

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>
Rasmond
  • 442
  • 4
  • 15