How to list the possible values of a string literal union type in TypeScript

String literals are one of the most straightforward and powerful tools of TypeScript. But until v3.4, it was impossible to list its accepted values. Developers needed to fall back to enums. Not a problem anymore.

String literals are one of the most straightforward and powerful tools of TypeScript. But until v3.4, it was impossible to list its accepted values. Developers needed to fall back to enums. Not a problem anymore.

On an imaginary page, there is a certain set of user types, namely Admin, Editor, Reader and Anonymous. The UserType is defined the following way.

type UserType = 'Admin' | 'Editor' | 'Reader' | 'Anonymous';

What if, for any purposes, we need to list all the values this type might take?

TL;DR

The solution is

export const UserTypes = ['Admin', 'Editor', 'Reader', 
'Anonymous'] as const; 
type UserType = typeof UserTypes[number]; 

Breakdown

It is not possible iterate on a type, but there is one type where we certainly can.

export const UserTypes = ['Admin', 'Editor', 'Reader', 'Anonymous'];

Now we have an array, what we can iterate through, but we don't have a type anymore. So we need to make a type, that takes the values from this array. Let's try the following code.

export const UserTypes = ['Admin', 'Editor', 'Reader', 'Anonymous'];
type UserType = typeof UserTypes[0] | typeof UserTypes[1] | typeof UserTypes[2] | typeof UserTypes[3]; // result: UserTypes: string[]

It is not elegant; if we need to add a new type of user, we have to add it in two places; once in the array and once in the type.

But the bigger problem is, the type UserType is just an array of string. In other words, the type we created takes in every string, from a mistyped editr to a superadmin type.

When we define an array as we did, the compiler looks at the values and says it is an array of strings. We can add, remove and edit elements as long as all the elements are strings. But this is not what we want now. We need to tell the compiler that this is not an array of any strings but an array that contains the elements Admin, Editor, Reader, and Anonymous. That implies that we don't want to change the order of the elements, edit, or remove anything in runtime. We can use the const assertion.

export const UserTypes = ['Admin', 'Editor', 'Reader', 
'Anonymous'] as const 
// result UserTypes: readonly ['Admin', 'Editor', 'Reader', 'Anonymous'];

type UserType = typeof UserTypes[0] | typeof UserTypes[1] | typeof UserTypes[2] | typeof UserTypes[3]; 
// result: type UserType = "Admin" | "Editor" | "Reader" | "Anonymous"

Now, if I try to add a type superadmin, I get an error: 'Property 'add' does not exist on type 'readonly ["Admin", "Editor", "Reader", "Anonymous"]'.'

Finally, the UserType is exactly what we want, and now we have an array to iterate on. The only problem is the hard-coded indexing in the type definition. Fortunately, there is a shortcut for this too.

export const UserTypes = ['Admin', 'Editor', 'Reader', 
'Anonymous'] as const 
// result UserTypes: readonly ['Admin', 'Editor', 'Reader', 'Anonymous'];

type UserType = typeof UserTypes[number]; 
// result: type UserType = "Admin" | "Editor" | "Reader" | "Anonymous"

typeof UserTypes[number] tells the compiler to get the types of values from UserTypes, that can be retrieved using any possible number.

Summary

  • We defined an array for the values that the UserType can take.
  • This array exists in runtime too so we can log, or show these values on the screen.
  • Using this array we were able to define our UserType