Adrian Guerrero
Adrian Guerrero

Reputation: 810

Use Typescript enum values to create new type

I have the following scenario:

enum FieldsMap {
  User = "user-name",
  Password = "user-password",
  Country = "user-country"
}

type Fields = "user-name" | "user-password" | "user-country";

as you can see Fields is repeating the values of FieldsMap, is there a way that Fields can use the values of FieldsMap in order to avoid repetition?. Also, I'm using FieldsMap as an enum here, but I can change that if necessary, I'm just trying to avoid using strings as much as possible:

const onChangeHandler = (key: Fields, value: string) => {
  switch (key) {
    case FieldsMap.User:
      // do something with `value`
      break;
    case FieldsMap.Password:
      // do something with `value`
      break;
    case FieldsMap.Country:
      // do something with `value`
      break;
  }
};

Upvotes: 28

Views: 15233

Answers (2)

jcalz
jcalz

Reputation: 328097

Update for TS4.1+: as the other answer points out, you can now use template literal types to get the string representation of the values of the FieldsMap enums:

type Fields = `${FieldsMap}`;
// type Fields = "user-name" | "user-password" | "user-country"

But it's important to be sure you actually need this; the ordinary use case for enums is that you don't want much of a dependency on the actual string literal values themselves; for an enum named Foo, the type Foo is already a union of the enum values, but they are treated nominally so you can't accidentally use the string in place of it. If you really want to use the string in place of the value, it's possible you don't want an enum in the first place and should just use objects with string literals in them. The point is: make sure this is what you really want before using it.


Just writing this out as an answer... in TypeScript, enums essentially already have the behavior you're looking for without needing to define any additional types.

That is, the following enum definition:

enum Fields {
  User = "user-name",
  Password = "user-password",
  Country = "user-country"
}

brings into scope a value named Fields, with properties whose keys are User, Password, and Country, and whose values are "user-name", "user-password", and "user-country", respectively, as you know. This value makes it to runtime, as in:

const x = Fields.User; // makes it to the emitted JS

and is similar to the value:

const FieldsLike = {
  User: "user-name",
  Password: "user-password",
  Country: "user-country"
} as const;

const xLike = FieldsLike.User; // makes it to the emitted JS;

But it also brings into scope a type named Fields, which is equivalent to the union of the property values in the Fields object. This type is erased with the rest of the type system when JS is emitted, but can be accessed at design time via type annotations and using IntelliSense, as in:

const y: Fields = x; // the "Fields" annotation is the type

and is similar to the type

type FieldsLike = (typeof FieldsLike)[keyof typeof FieldsLike];
// type FieldsLike = "user-name" | "user-password" | "user-country"

const yLike: FieldsLike = xLike; // the "FieldsLike" annotation is the type

This is the type you're looking for; see below.

By the way, the enum also acts as a namespace with exported types for each enum member:

const z: Fields.Password = Fields.Password; // type and value

(so the Fields.Password on the left is the name of a type, while the Fields.Password on the right is the name of a value). Thus the types accessible under Fields are similar to the namespace:

namespace FieldsLike {
  export type User = typeof FieldsLike.User;
  export type Password = typeof FieldsLike.Password;
  export type Country = typeof FieldsLike.Country;
}

const zLike: FieldsLike.Password = FieldsLike.Password; // type and value

Whew, that means just using the enum Fields { ... } definition give us the behavior of const FieldsLike = ..., type FieldsLike = ..., and namespace FieldsLike all at once.

Let's back up... the type you were looking for, the union of all properties under the Fields enum, is already a type named Fields. (Okay, I changed the name of your FieldsMap to Fields), and you can use it directly:

const onChangeHandler = (key: Fields, value: string) => {
  switch (key) {
    case Fields.User:
      // do something with `value`
      break;
    case Fields.Password:
      // do something with `value`
      break;
    case Fields.Country:
      // do something with `value`
      break;
  }
};

Okay, hope that helps. Good luck!

Link to code

Upvotes: 65

PJCHENder
PJCHENder

Reputation: 6010

After the Template Literal Types released, you can just use:

type Fields = `${FieldsMap}` // "user-name" | "user-password" | "user-country"

Upvotes: 23

Related Questions