Skip to content

Unions and narrowing ​

A union of different kinds of values (number | string, string[] | boolean | null) is stored as one 64-bit word with a small type tag. Narrowing with typeof, ===, instanceof, Array.isArray, truthiness or a switch on a discriminant is a tag test, and inside the narrowed branch the value is unboxed again.

ts
// Unions of different kinds narrowed by typeof, Array.isArray and ===; a discriminated union
// narrowed by switch.
function describe(v: number | string | boolean[] | null): string {
  if (v === null) return "nothing";
  if (typeof v === "number") return `number ${v * 2}`;
  if (typeof v === "string") return `string ${v.toUpperCase()}`;
  return `flags ${v.filter((b: boolean): boolean => b).length}/${v.length}`;
}

console.log(describe(21), describe("hi"), describe([true, false, true]), describe(null));

type Event =
  | { kind: "click"; x: number; y: number }
  | { kind: "key"; code: string }
  | { kind: "quit" };

function handle(e: Event): string {
  switch (e.kind) {
    case "click":
      return `click at ${e.x},${e.y}`;
    case "key":
      return `key ${e.code}`;
    case "quit":
      return "bye";
  }
}

const events: Event[] = [
  { kind: "click", x: 3, y: 4 },
  { kind: "key", code: "Enter" },
  { kind: "quit" },
];
for (const e of events) console.log(handle(e));

// An un-narrowed union can still be printed, compared, and defaulted.
const maybe: string | number | undefined = events.length > 2 ? 42 : undefined;
console.log(maybe ?? "none", maybe === 42);
text
number 42 string HI flags 2/3 nothing
click at 3,4
key Enter
bye
42 true

What works on an un-narrowed union ​

Printing, String(x), template interpolation, === / !==, typeof, ?? and truthiness work on any union. Anything else (x + 1, x.length, calling a method) needs one kind first; the compiler rejects it with CS1239 and suggests the narrowing.

Unions of object types are one object type over their common fields, so a discriminated union like Event above needs no tag word at all: e.kind is an ordinary field read.

Limits today ​

  • A union no runtime tag can tell apart, such as two different array types, is rejected (CS1233), as is a union used as a Map or Set key.
  • Passing a number[] where a (number | string)[] is expected would let the callee write a string into your number array, so it is rejected (CS1240). Copy it into a new array of the wider type.
  • as casts that narrow are not allowed; narrowing is done with runtime checks, which tsc then follows.

Next: Generics.