ZetCode

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.

declaring_unions.ts
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.

union_error.ts
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.

union_narrowing.ts
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.

union_arrays.ts
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.

union_literals.ts
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.

union_objects.ts
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.

union_function_params.ts
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.

union_optional.ts
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.

distributive_conditional_types.ts
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.

union_utilities.ts
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.

intersection_operator.ts
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

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all TypeScript tutorials.