Skip to content
Theory · Unions and literal types · lesson 5 of 8

Literal types

On a shop's website, you choose a clothing size from a dropdown: S, M or L. You cannot type "XL" or "m" into it—the dropdown offers only its own choices. With a plain text field instead, a buyer could type anything, and the shop would have to guess what size "em" means.

A parameter of type string is that text field. The function order(size: string) accepts both "M" and the typo "m", and tsc stays silent. We need a type that works like a dropdown.

You have already seen a literal type: const b = 5 has the type 5, which holds exactly one value. Put several such types into a union, and you get a list of choices.

A list of literals

A union of literal types lists every allowed value. To avoid repeating it everywhere, you give it a name with the type keyword:

TypeScript
type Size = "S" | "M" | "L";

function order(size: Size) {
    console.log("size " + size);
}

order("M"); // size M
order("L"); // size L

Let's walk through the example line by line:

  1. type Size = "S" | "M" | "L"; declares a name: Size now stands for "S", "M" or "L".
  2. order(size: Size) accepts only these three strings.
  3. order("M") and order("L") pass values from the list and print size M and size L.

The name Size creates nothing new at run time. It is only a name for a type, and tsc erases it along with the other types. But the editor knows the list of choices: inside the quotes in order("") it suggests "S", "M" and "L".

A value not on the list

The name Size works for a variable too. Let's try to store a size that is not on the list:

TypeScript
type Size = "S" | "M" | "L";

let mySize: Size = "M";
mySize = "XL"; // ✗
TEXT
error TS2322: Type '"XL"' is not assignable to type 'Size'.

In the message, '"XL"' is the literal type of the value "XL". tsc says that this type is not part of Size. tsc rejects a typo like "m" too, and even asks Did you mean '"M"'?: TypeScript tells uppercase from lowercase.

Here the dropdown picture ends. A dropdown on a website checks the choice while the page is running, while a literal type is checked only at compile time. A string that arrives from a form or a server while the program runs is something tsc cannot check: at run time the types are gone.

Interviewers like to ask this: how does "a" | "b" differ from string? The answer: string allows any string, while "a" | "b" allows exactly two, and tsc finds a typo before the program runs.

Common mistake

The size is first stored in a let variable and then passed to the function:

TypeScript
type Size = "S" | "M" | "L";

function order(size: Size) {
    console.log("size " + size);
}

let choice = "M";
order(choice); // ✗
TEXT
error TS2345: Argument of type 'string' is not assignable to parameter of type 'Size'.

choice holds "M", yet tsc still reports an error. The reason is type widening: a let variable can later get another string, so TypeScript infers the type string for it, not "M". The whole of string does not fit the Size parameter. A literal written right in the call, like order("M"), keeps its type, so there is no error there.

There are two ways to fix it:

TypeScript
type Size = "S" | "M" | "L";

function order(size: Size) {
    console.log("size " + size);
}

const first = "M";
let second: Size = "L";
order(first);  // size M
order(second); // size L

Declare it with const

const first = "M" will never change, so TypeScript keeps its literal type "M". That type is part of Size, and the call compiles.

Write the type explicitly

let second: Size = "L" stays a variable, but the annotation lets it hold only values from Size. This is handy when the size changes later.

Key points

  • The literal type "M" allows exactly one value, and the union "S" | "M" | "L" is a list of choices.
  • type Size = … gives the union a name; it is erased at compile time.
  • tsc rejects a value that is not on the list: Type '"XL"' is not assignable to type 'Size'.
  • let choice = "M" gets the type string and does not fit Size; const or a : Size annotation fixes it.