Union and literal types

TypeScript · Essentials · 10 min

Unions of literals replace magic strings and model the 'one of these' choices that appear in every domain.

A union type A | B means a value can be either. string | number accepts both; User | null is how you model "maybe there's a user".

A literal type is a single exact value used as a type: "left", 42, true. On their own they're odd, but unions of literals are one of TypeScript's best features:

type Align = "left" | "center" | "right";
function align(text: string, how: Align) { ... }
align("hi", "centre"); // error: not assignable to Align

You get autocompletion for the allowed values and a compile error for typos, at zero runtime cost — the values stay ordinary strings. The same works for numbers (type Dice = 1 | 2 | 3 | 4 | 5 | 6) and boolean is literally true | false.

What can you do with a union value? Only the operations valid for every member. With id: string | number you can call id.toString() (both have it) but not id.toUpperCase(). To use member-specific operations you narrow the union first, typically with a check like typeof id === "string" — that's the next lesson.

The type keyword creates an alias — a name for any type expression, including unions, which interfaces can't express.

Remember literal widening: let how = "left" is a string, so passing it where Align is expected fails. Either annotate (let how: Align = "left") or use const.

Unions also apply to arrays: (string | number)[] is an array whose elements can each be either, while string[] | number[] is either an all-string or an all-number array.

Example

type Align = "left" | "center" | "right";
type Id = string | number;

function pad(text: string, width: number, how: Align = "left"): string {
  if (how === "left") return text.padEnd(width);
  if (how === "right") return text.padStart(width);
  const space = width - text.length;
  return " ".repeat(Math.floor(space / 2)) + text + " ".repeat(Math.ceil(space / 2));
}

function describe(id: Id): string {
  return `#${id.toString()}`;      // OK: both members have toString
}

console.log(`[${pad("hi", 6, "center")}]`, describe(7), describe("x7"));

let choice = "right";              // string, not "right"
// @ts-expect-error -- Argument of type 'string' is not assignable to 'Align'
pad("hi", 6, choice);
const fixed = "right";             // "right"
pad("hi", 6, fixed);

Remember

  • A | B = a value of either type
  • Unions of string literals replace magic strings with zero runtime cost
  • On a union you can only use what every member supports — narrow first

Watch out for

  • Losing a literal type through let and then failing to pass it to a function expecting the union.

Check yourself

  1. Given x: string | number, which expression compiles without narrowing?
  2. What is the difference between (string | number)[] and string[] | number[]?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types