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

Find out if you can allow string literals with the enum type in TypeScript

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 (which is the intended behavior of enums in TypeScript). Unfortunately, there's no way of allowing string literals with enums (at least not without running into complexities).

Using Const Assertions to Create an Enum-Like Object

One workaround to our problem could be to use const assertions to create an enum-like object. 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']

Hope you found this post useful. It was published . Please show your love and support by sharing this post.