Union types (|)

Accept a value that may be any one of several types, then narrow before using branch-specific members.

Signature
type A = X | Y | Z;
function printId(id: number | string): void;
type Maybe<T> = T | null | undefined;
  • Added in TypeScript 1.4

Parameters

ParameterTypeDescription
$membersX | Y | ...Two or more constituent types separated by `|`. Order is irrelevant and duplicates collapse, so `string | number` and `number | string | string` are the same type.
$leading baroptional| X | YAn optional leading `|` before the first member, permitted so multi-line unions line up.

Errors

tsc reports "Property 'x' does not exist on type 'A | B'" when you access a member that is not present on every constituent, and "Type 'A | B' is not assignable to type 'A'" when passing an unnarrowed union where one branch is expected.

Changelog

  • PHP 2.0Under `strictNullChecks`, `null` and `undefined` stopped being members of every type and became union members you must write and narrow explicitly.
  • PHP 3.2Object spread and rest on generic unions improved, so spreading a union-typed object no longer collapses to a single branch.
  • PHP 4.6Dependent parameter narrowing: destructuring a discriminated union in a parameter list narrows the remaining elements of the union.

A union type is formed from two or more other types and represents a value that may be any one of them. The rule that governs everything else follows from that: TypeScript only lets you do what is valid for every member. number | string has toString, because both have it, but not toFixed and not toUpperCase.

Narrowing is the whole workflow

To use branch-specific members you narrow the union with ordinary runtime code the compiler understands: typeof for primitives, Array.isArray, instanceof for classes, the in operator for structural checks, and equality on a discriminant property for object unions. Inside the true branch the type is narrowed to that constituent.

For unions of object shapes, a shared literal-typed discriminant (kind, type, ok) is worth designing for deliberately. It turns a switch into exhaustive, checkable dispatch and is far more robust than probing for the presence of properties.

When to reach for it

Use a union when a value genuinely has several distinct shapes: a result that is either success or failure, an argument accepted in two forms, a state machine's states. Do not use it to paper over a value you have not modelled — if every call site immediately narrows to one branch, you probably wanted two functions.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Accept an id that callers may pass as either a number or a string.
  • Model an operation result as success-with-value or failure-with-error and force the caller to check.
  • Represent a nullable field under `strictNullChecks` as `T | null` so every read must be guarded.
  • Enumerate the states of a component so each one carries only the data that state actually has.

Frequently asked questions

Why can't I access a property that exists on one member of my union?

Because a union only exposes what every constituent has. TypeScript cannot know which branch you hold at that line, so a property present on only one member is an error. Narrow first with `typeof`, `instanceof`, `in`, or a check on a discriminant property.

What is the difference between a union and an intersection?

A union `A | B` is a value that is A or B, so you get only the members they share. An intersection `A & B` is a value that is A and B at once, so you get the members of both. Unions widen the set of accepted values and narrow the accessible API; intersections do the reverse.

How do I make sure I handled every member of a union?

Assign the narrowed value to a variable of type `never` in the `default` branch of a `switch` on the discriminant. If every case is handled the value really is `never` and it compiles; add a new member and the assignment fails, pointing you at the unhandled case.

More examples

A result union the caller cannot forget to check

Prints "port 8080" then "bad port: 70000". The literal `true`/`false` on `ok` is the discriminant: before the check, neither `value` nor `error` is accessible, so the ternary is not defensive style but the only thing that compiles. This is how you get the ergonomics of exceptions-as-values without a library.

Example · typescript
type Result<T> = { ok: true; value: T } | { ok: false; error: string };

function parsePort(raw: string): Result<number> {
  const n = Number(raw);
  if (!Number.isInteger(n) || n < 1 || n > 65535) {
    return { ok: false, error: `bad port: ${raw}` };
  }
  return { ok: true, value: n };
}

for (const raw of ["8080", "70000"]) {
  const r = parsePort(raw);
  // r.value is unreachable until r.ok has been checked.
  console.log(r.ok ? `port ${r.value}` : r.error);
}

Narrowing object unions with `in` and `instanceof`

Prints "text of 11 chars", "blob of 2048 bytes", "error 404". `instanceof` narrows the class member, `in` narrows between the two structural members, and the final `return` needs no check because elimination has left exactly one constituent. Reorder these checks and the code stops compiling, which is the point.

Example · typescript
class HttpError extends Error {
  constructor(public status: number) { super(`HTTP ${status}`); }
}
type Payload = { text: string } | { blobSize: number } | HttpError;

function describe(p: Payload): string {
  if (p instanceof HttpError) return `error ${p.status}`;
  if ("text" in p) return `text of ${p.text.length} chars`;
  return `blob of ${p.blobSize} bytes`;
}

console.log(describe({ text: "hello world" }));
console.log(describe({ blobSize: 2048 }));
console.log(describe(new HttpError(404)));

Discussion

  • Be the first to comment on this lesson.