Is It Possible to Allow Literal String Values With TypeScript's Enum Type?

Enum allows us to define a set of named constants which makes it easier to create a set of distinct cases. Anything declared as the enum type only accepts enum members as valid inputs. This means that literal strings (even if they match the values of enum members) would be considered invalid. To illustrate this, let's look at the following example:

enum Answer {
    Yes = 'accepted',
    No = 'not-accepted',
};

const subscribe = (answer: Answer) => {
    // do something...
};

// works:
subscribe(Answer.Yes);
subscribe(Answer.No);

// error: Argument of type '"accepted"' is not assignable to parameter of type 'Answer'
subscribe('accepted');

As you can see in the example above, string literals are not allowed with enums. This is the intended behavior of enums in TypeScript because:

  1. Other code does not need to know about the inner implementation, or the value of an enum member;
  2. It enforces you to use enum members instead of string literals;
  3. It makes it easier to update your code (for example, when you update an enum member's value, there would only be one place you would have to make the change).

Before you decide to allow string literals as valid inputs for your enums, you may want to re-think your design approach as it defeats the whole purpose of using an enum. If you have a valid use case for it, then depending on the version of TypeScript you're using you may have the following choices:

In versions prior to 3.4, there's no way of allowing string literals with enums (at least not without running into complexities).

Using Template Literal Types

Introduced in TypeScript 4.1, template literal types produce a new string literal type by concatenating the contents. When an enum is used as a template literal type, it allows enum members as well as their corresponding values to be used as valid inputs (because it expands and concatenates their values into unions of string literals). For example:

// TypeScript 4.1+
enum Answer {
    Yes = 'accepted',
    No = 'not-accepted',
};

type AnswerType = `${Answer}`;

const subscribe = (answer: AnswerType) => {
    // do something...
};

// works:
subscribe(Answer.Yes);
subscribe(Answer.No);
subscribe('accepted');
subscribe('not-accepted');

Please note that since template literal types expand values into unions of strings, if you use any non-string value, they will be converted into a string as well which could lead to unexpected results.

In the example code above, AnswerType is expanded into a union of string literal types (derived from the enum Answer) like so:

type AnswerType = 'accepted' | 'not-accepted';

Enum With Members Having Non-String Values:

Let's look at the following example to demonstrate how template literal types work with enums whose members have non-string values:

// TypeScript 4.1+
enum Answer {
    Yes = 1,
    No = 2,
};

type AnswerType = `${Answer}`;

const subscribe = (answer: AnswerType) => {
    // do something...
};

// error: Argument of type '...' is not assignable to parameter of type '"1" | "2"'.
subscribe(Answer.Yes);
subscribe(Answer.No);
subscribe(1);
subscribe(2);

// works:
subscribe('1');
subscribe('2');

As you can see, the only accepted values are numeric strings '1' and '2'. This happens because AnswerType is expanded into a union of string literal types (derived from the enum Answer) like so:

type AnswerType = '1' | '2';

Using Const Assertions to Create an Enum-Like Object

Introduced in TypeScript 3.4, you can use const assertions to create an enum-like object which would allow string literals to be used as valid inputs. Let's re-write our example using const assertion so that string literals are allowed as well:

// TypeScript 3.4+
const Answer = {
    Yes: 'accepted',
    No: 'not-accepted',
} as const;

type Answer = (typeof Answer)[keyof typeof Answer];

const subscribe = (answer: Answer) => {
    // do something...
};

// works:
subscribe(Answer.Yes);
subscribe(Answer.No);
subscribe('accepted');
subscribe('not-accepted');

Now, string literals as well as object properties are considered valid, and it correctly throws an error for invalid values, for example:

// error: Argument of type '"foobar"' is not assignable to parameter of type 'Answer'.
subscribe('foobar');

How Does This Work?

Using const assertions on an object literal does the following:

  1. Makes the object properties readonly;
  2. Allow us to create new literal expressions;
  3. Literal types are not widened.

Without using const assertions, the interpretation of an object type would have been like so:

const Answer = {
    Yes: 'accepted', // type: string
    No: 'not-accepted', // type: string
};

As you can see, the type is widened to a string and not to the literal strings "accepted" and "not-accepted". This isn't very type-safe (as all we can guarantee is that the property is a string). Not only that, but the property types can be reassigned as well, even though the object is declared as a const.

Now, let's see how this changes with using const assertion:

const Answer = {
    Yes: 'accepted', // (readonly) type: 'accepted'
    No: 'not-accepted', // (readonly) type: 'not-accepted'
} as const;

Now each property is readonly and has a specific/literal type (which is not widened to a generic type such as string). With that, defining a type that allows string literals as well as object properties becomes quite easy:

type T = typeof Answer; // object (Answer)
type K = keyof T; // 'Yes' | 'No'

type Answer = (T)[K]; // Answer['Yes'] | Answer['No']

This post was published (and was last revised ) by Daniyal Hamid. Daniyal currently works as the Head of Engineering in Germany and has 20+ years of experience in software engineering, design and marketing. Please show your love and support by sharing this post.