112

Given the following:

enum FooKeys {
  FOO = 'foo',
  BAR = 'bar',
}

I'd like to make an interface like this one, but instead of defining keys by hand, build it out of enum's values.

interface Foo {
  foo: string
  bar: string
}

Is something like this possible with TypeScript?

Thanks!

4 Answers 4

122

How to build a type from enum values in TypeScript?

An enum can hold string and number values.

For strings: you can create a string union from enum values using a template string

enum FooKeys {
  FOO = 'foo',
  BAR = 'bar',
}

type FooValues =`${FooKeys}`; // this equals 'foo' | 'bar'

For numbers: we can either create string union the same way or: starting TS 5.0

TypeScript 5.0 manages to make all enums into union enums by creating a unique type for each computed member. That means that all enums can now be narrowed and have their members referenced as types as well.

Which means:

enum MagicNumbers {
a = 1,
b = 42
}

const numberA : MagicNumbers = 1;
const numberB : MagicNumbers = 2; // will raise an error since TS 5.0

Experiment by yourself

For both: Combining the above, we can build an EnumAsUnion type helper as follows

enum-as-union.ts

type StringValues<T> = {
  [K in keyof T]: T[K] extends string ? T[K] : never;
}[keyof T];

type NumberValues<T> = {
  [K in keyof T]: T[K] extends number ? T[K] : never;
}[keyof T];

/**
 * Usage : type EnumValues = EnumAsUnion<typeof anEnum>
 */
type EnumAsUnion<T> = `${StringValues<T>}` | NumberValues<T>;

Example:

import { EnumAsUnion } from 'enum-as-union';

enum anEnum {
  val1 = 'a',
  val2 = 'b',
  val3 = 1,
  val4 = 2,
}

type EnumValues = EnumAsUnion<typeof anEnum>;

let t: EnumValues;
t = 'a';
t = 'b';
t = 'c'; // error, as expected

t = 1;
t = 2;
t = 3; // error, as expected

Try it here

Sign up to request clarification or add additional context in comments.

3 Comments

really nice and clean, thanks
Is there another way to have a type for the first scenario? My enum's values are strings and I would like to have a new type that I can hold the values and another string hardcoded value like const a: newType = 'my-type'; where type newType = valuesof oldType | 'my-type';
AFAIK there is no way to extend an existing enum adding new values. We have to redefined a new one. Otherwise to just add a string to your existing string union you are close: type newType = oldType | 'my-type'; There is no way to create a runtime value from a (compilation time) typescript type.
108

Yes, you can use enum values as keys. And you can use a mapped type like the standard library's Record<K, V> to prevent repetition:

enum FooKeys {
  FOO = 'foo',
  BAR = 'bar',
}

// probably all you need, but it's a type alias
type FooType = Record<FooKeys, string>;

// if you need an interface instead you can do this
interface FooInterface extends FooType {};

And you can verify that it works:

declare const foo: FooInterface;
foo.foo; // okay
foo[FooKeys.FOO]; // okay

foo.bar; // okay
foo[FooKeys.BAR]; // okay

foo.baz; // error

Does that work for you? Good luck!

3 Comments

Thank you for a detailed answer. This is exactly what I needed!
What is Record in Record<FooKeys, string>? Why did you decide to add yet another arbitrary type in your app/code/module? Why do you need extra type?
It's a utility type, the definition of which in the standard library is linked in the answer above.
11

[@hackape 's solution][1] is great, but I found minimal duplication extending his solution as below:

type ReverseMap<T extends Record<keyof T, any>> = {
  [V in T[keyof T]]: {
    [K in keyof T]: T[K] extends V ? K : never;
  }[keyof T];
}

const Map = {
  'FOO': "foo" as "foo",
  'BAR': "bar" as "bar",
}

const reverseMap: ReverseMap<typeof Map> = Object.entries(Map).reduce((rMap, [k, v]) => {
  rMap[v] = k;
  return rMap;
}, {} as any);

export type Values = keyof typeof reverseMap; // 'foo' | 'bar';

ReverseMap implementation is well explained [here][2]

[1]: https://stackoverflow.com/a/60768453/5519365 [2]: https://stackoverflow.com/a/55209457/5519365

Update: I found a much simpler solution for ReverseMap

const Obj = {
 FOO: 'foo',
 BAR: 'bar',
} as const;

type ReverseMap<T> = T[keyof T];

export type Values = ReverseMap<typeof Obj>; // 'foo' | 'bar';

1 Comment

Do you know what's the difference between keyof typeof myType vs `${myType}`? I see the latter in @flavien-volken's answer and it works for string values.
7

Does this answer your question?

enum FOO_BAR {
    F = "foo",
    B = "bar",
}

type FooType = Record<FOO_BAR, string>;

const obj: FooType = {
    // you can use enum values, better for refactoring
    [FOO_BAR.F]: "action foo",
    [FOO_BAR.B]: "action bar",

    // or use enum values
    // foo: "action foo",
    // bar: "action bar",
};

obj[FOO_BAR.F]; // -> "action foo"
obj["foo"];     // -> "action foo"

// If you want partial keys
type FooTypePartial = Partial<FooType>;

const objPartial: FooTypePartial = {
    [FOO_BAR.F]: "action foo",
};

objPartial["foo"]; // -> "action foo", may be undefined

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.