Union type in TypeScript: one value, several possible types

A union type lists the shapes a value may take, separated by a vertical bar: narrowing, discriminated unions and exhaustiveness checking.
3 min read
Believemy logo

An API response is either an invoice or an error message. A status is "draft", "sent" or "paid", and nothing else.

The union type exists to write exactly that, and then to force the code to deal with every case.


Definition

A union type lists the types a value is allowed to take, separated by a vertical bar. The value holds one of them at a time, and the compiler rejects anything that is not on the list.

JAVASCRIPT
type Status = "draft" | "sent" | "paid";

let invoiceStatus: Status = "paid";
invoiceStatus = "canceled";
// Type '"canceled"' is not assignable to type 'Status'.

type Identifier = string | number;

The three values of Status are not types in the usual sense: they are literal types, each accepting a single string. That is what neatly replaces a hand-rolled set of constants.


Narrowing

As long as the value remains a union, only the properties shared by every member are reachable. A single test shrinks the set, and the compiler follows the reasoning branch by branch.

JAVASCRIPT
function size(value: string | number): number {
  if (typeof value === "string") {
    return value.length;   // here, value is a string
  }
  return value;            // here, it can only be a number
}

Three operators drive this narrowing: typeof for primitives, instanceof for classes, and in to test for the presence of a property. An Strict equality (===) against a literal type works just as well.


The discriminated union

The most useful case in practice: every member carries a shared field whose value identifies it. Testing that one field is then enough to unlock the other properties of that member.

JAVASCRIPT
type ApiResponse =
  | { ok: true; invoice: { id: number } }
  | { ok: false; error: string };

function display(response: ApiResponse) {
  if (response.ok) {
    console.log(response.invoice.id);
  } else {
    console.log(response.error);
  }
}
Good to know

Reading response.invoice before the test is refused: the property does not exist on the failure member. The union makes impossible the oversight that, in plain JavaScript, produces a silent undefined.


Frequently asked questions

Question

How does it differ from an intersection?

A union is written with a vertical bar and means "one or the other"; an intersection is written with an ampersand and means "both at once". The first narrows what is reachable, the second widens it. Beginners routinely swap them, because "or" intuitively feels like the broader word.


Question

How can every case be guaranteed handled?

A switch on the discriminating field, with no default branch and an annotated return type: if a member is forgotten, the compiler reports a path that returns nothing. This is exhaustiveness checking, and it speaks up on the day a fourth status is added.


Question

Can a union contain null?

Yes, and in strict mode that is its most common use: string | null describes a value that may be missing, and the compiler demands a test before any read. Nullish coalescing (??) and Optional chaining (?.) then become the two tools that keep those tests short.

Related terms

Discover our javaScript glossary

Every word of JavaScript explained simply: keywords, built-in objects, methods, errors and concepts. Clear definitions and examples that actually run, to learn and to troubleshoot.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.