TypeScript Union Types
last modified October 15, 2026
A union type describes a value that can be one of several types.
It is written with the | operator: string | number
accepts strings and numbers. Unions model values that legitimately vary, such
as an ID that may be a number or a string, a result that may be a success or
an error, or a setting with a fixed set of options. They are one of the most
used features of TypeScript.
Declaring Unions
A variable with a union type can hold a value of any of the member types.
let value: string | number = "Hello"; console.log(value, typeof value); value = 10; console.log(value, typeof value); type Id = string | number; const ids: Id[] = ["a-1", 2, "c-3"]; console.log(ids);
The variable first holds a string and then a number. Assigning a
boolean would be an error. A type alias such as Id gives a union
a name, so it can be reused.
$ npx tsx declaring_unions.ts Hello string 10 number [ 'a-1', 2, 'c-3' ]
Working with Union Values
Without further checks, only operations that are valid for every member of the union are allowed.
function shout(value: string | number): string {
return value.toUpperCase();
}
function show(value: string | number): string {
return value.toString();
}
toUpperCase exists only on strings, so calling it on a
string | number is rejected. toString exists on both
types, so show compiles. To use members of one type, the union
must first be narrowed.
$ npx tsc --noEmit union_error.ts(2,18): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.
Union Narrowing
A runtime check such as typeof narrows the union to one
of its members inside the checked branch.
function printValue(value: string | number | boolean): void {
if (typeof value === "string") {
console.log(`string of length ${value.length}`);
} else if (typeof value === "number") {
console.log(`number rounded to ${value.toFixed(1)}`);
} else {
console.log(`boolean ${value ? "yes" : "no"}`);
}
}
printValue("Hello");
printValue(3.14159);
printValue(false);
In each branch, the compiler knows the exact type, so string and
number methods are available. In the last branch, only boolean
is left. Other narrowing techniques, such as instanceof,
in, and custom type predicates, are covered in the
type guards tutorial.
$ npx tsx union_narrowing.ts string of length 5 number rounded to 3.1 boolean no
Unions with Arrays
Where the parentheses are placed changes the meaning of a union with arrays.
const mixed: (string | number)[] = ["Hello", 42];
mixed.push(100, "TypeScript");
let uniform: string[] | number[] = ["a", "b"];
uniform = [1, 2, 3];
console.log(mixed);
console.log(uniform);
function total(values: string[] | number[]): number {
return values.length;
}
console.log(total(["x", "y"]), total([1, 2, 3]));
(string | number)[] is an array whose elements can each
be a string or a number, so mixed values are allowed. string[] |
number[] is either an array of only strings or an array of only
numbers; ["a", 1] could not be assigned to it. Common array
members such as length can be used on both kinds.
$ npx tsx union_arrays.ts [ 'Hello', 42, 100, 'TypeScript' ] [ 1, 2, 3 ] 2 3
Literal Unions
A union of literal types restricts a value to a fixed set of options.
type Direction = "north" | "south" | "east" | "west";
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
function move(direction: Direction, steps: DiceRoll): string {
return `Moving ${direction} by ${steps}`;
}
console.log(move("north", 3));
console.log(move("west", 6));
// move("up", 7); // Error: '"up"' is not assignable to parameter of type 'Direction'
Only the listed strings and numbers are accepted. Editors suggest the valid values, and the compiler rejects misspellings. Literal unions are a lightweight alternative to enums and need no runtime code.
$ npx tsx union_literals.ts Moving north by 3 Moving west by 6
Discriminated Unions
A union of object types that share a literal property, the discriminant, can be narrowed by checking that property.
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };
type Triangle = { kind: "triangle"; base: number; height: number };
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
const shapes: Shape[] = [
{ kind: "circle", radius: 5 },
{ kind: "square", side: 4 },
{ kind: "triangle", base: 6, height: 3 },
];
for (const shape of shapes) {
console.log(shape.kind, getArea(shape).toFixed(2));
}
In each case, shape is narrowed to one
member of the union, so only its properties can be accessed. Because all
three kinds are handled, the compiler knows that the function always returns
a number. If a new shape were added to the union without a new case, the
function would fail to compile.
$ npx tsx union_objects.ts circle 78.54 square 16.00 triangle 9.00
Unions in Function Parameters and Results
Functions often accept a union to be more flexible, and return a union when the result can take different forms.
function formatValue(value: string | number | Date): string {
if (value instanceof Date) {
return value.toISOString().slice(0, 10);
}
if (typeof value === "number") {
return value.toFixed(2);
}
return value.toUpperCase();
}
function parseAge(text: string): number | Error {
const age = Number(text);
return Number.isInteger(age) && age >= 0 ? age : new Error(`invalid age: ${text}`);
}
console.log(formatValue("hello"));
console.log(formatValue(3.14159));
console.log(formatValue(new Date(2026, 9, 15, 12)));
for (const input of ["42", "-3"]) {
const result = parseAge(input);
console.log(result instanceof Error ? result.message : `age ${result}`);
}
formatValue handles three types, each in its own branch.
parseAge returns either a number or an Error; the
caller must check which one it got before using it as a number. Returning a
union makes the possible failure part of the function's type, which a thrown
exception is not.
$ npx tsx union_function_params.ts HELLO 3.14 2026-10-15 age 42 invalid age: -3
Optional and Nullable Values
Unions with undefined and null describe
values that may be missing. An optional property is related, but not the same
as a property whose type includes undefined.
type Profile = {
name: string;
nickname?: string;
website: string | undefined;
manager: string | null;
};
const alice: Profile = { name: "Alice", website: undefined, manager: null };
const bob: Profile = { name: "Bob", nickname: "B", website: "bob.dev", manager: "Alice" };
for (const p of [alice, bob]) {
console.log(`${p.nickname ?? p.name}: ${p.website ?? "no website"}, manager ${p.manager ?? "none"}`);
}
console.log("nickname" in alice, "website" in alice);
The optional nickname property may be left out
entirely. The website property must always be present, even if
its value is undefined; omitting it would be an error. The
in checks show the difference at runtime. null
usually marks a value that is intentionally empty, as for
manager. The ?? operator provides defaults for all
of them. See the null and
undefined tutorial.
$ npx tsx union_optional.ts Alice: no website, manager none B: bob.dev, manager Alice false true
Distributive Conditional Types
A conditional type applied to a union is evaluated separately for each member, and the results are combined into a new union.
type ElementType<T> = T extends readonly (infer U)[] ? U : T; type Inputs = number[] | string[] | boolean; type Elements = ElementType<Inputs>; const values: Elements[] = [1, "two", true]; console.log(values); type ToArray<T> = T extends unknown ? T[] : never; type Arrays = ToArray<string | number>; const a: Arrays = ["only", "strings"]; const b: Arrays = [1, 2, 3]; console.log(a, b);
ElementType<number[] | string[] | boolean> is
computed as ElementType<number[]> | ElementType<string[]> |
ElementType<boolean>, which gives number | string |
boolean. ToArray<string | number> distributes the
same way and produces string[] | number[], not
(string | number)[], so a and b must
each contain only one kind of value.
$ npx tsx distributive_conditional_types.ts [ 1, 'two', true ] [ 'only', 'strings' ] [ 1, 2, 3 ]
Filtering Unions
The built-in Exclude and Extract types use
distribution to remove or keep members of a union.
type Status = "draft" | "review" | "published" | "archived"; type VisibleStatus = Exclude<Status, "draft" | "archived">; type FinalStatus = Extract<Status, "published" | "archived">; type Primitive = Extract<string | number | (() => void) | boolean, string | number | boolean>; const shown: VisibleStatus[] = ["review", "published"]; const done: FinalStatus = "archived"; const config: Primitive[] = ["dark", 14, true]; console.log(shown, done, config);
VisibleStatus is "review" | "published"
and FinalStatus is "published" | "archived".
Primitive drops the function type and keeps the three primitive
types. Deriving such types from one source union keeps them in sync when the
union changes.
$ npx tsx union_utilities.ts [ 'review', 'published' ] archived [ 'dark', 14, true ]
Unions versus Intersections
An intersection type, written with &, is the
opposite of a union: the value must satisfy all member types at once.
interface Person {
name: string;
age: number;
}
interface Developer {
language: string;
}
type Programmer = Person & Developer;
type PersonOrDeveloper = Person | Developer;
const programmer: Programmer = { name: "Jan", age: 35, language: "TypeScript" };
const someone: PersonOrDeveloper = { language: "Go" };
function describe(x: PersonOrDeveloper): string {
return "name" in x ? `person ${x.name}` : `developer using ${x.language}`;
}
console.log(programmer);
console.log(describe(programmer), "|", describe(someone));
A Programmer must have all three properties. A
PersonOrDeveloper needs to match only one member, so an object
with just a language is valid. Thinking of types as sets of
values helps: a union of two types contains the values of both, while an
intersection contains only values that belong to both. Because
programmer has a name, describe treats
it as a person.
$ npx tsx intersection_operator.ts
{ name: 'Jan', age: 35, language: 'TypeScript' }
person Jan | developer using Go
Best Practices
- Narrow Before Use: Check the type with
typeof,instanceof,in, or a discriminant before using members of one type. - Prefer Discriminated Unions: Give object variants a common literal property such as
kindinstead of telling them apart by optional properties. - Use Literal Unions for Options: They are type-safe, readable, and produce no runtime code.
- Mind the Parentheses:
(A | B)[]andA[] | B[]are different types. - Derive Related Unions: Use
ExcludeandExtractinstead of repeating member lists.
Source
TypeScript Handbook: Union Types
This tutorial covered union types in TypeScript. A union says that a value is one of several types, and narrowing tells the compiler which one it is at a given point in the code.
Author
List all TypeScript tutorials.