ZetCode

TypeScript Unions

Last modified November 12, 2025

Unions in TypeScript allow a variable to be one of several types. They are declared using the | operator. Unions provide flexibility and power in type checking, making it possible to write more robust code.

Unions in TypeScript are a way to define a variable that can be one of several types. For example, a variable can be either a string or a number.

Declaring Unions

This example demonstrates how to declare a union in TypeScript.

declaring_unions.ts
let value: string | number = "Hello";
value = 10;

console.log(value);  // Output: 10

The value variable is declared to be of type string or number. It is initially assigned the string value "Hello", but can later be assigned the number value 10.

Union Narrowing

TypeScript uses a process called union narrowing to determine the actual type of a union variable.

union_narrowing.ts
function printValue(value: string | number) {
    if (typeof value === "string") {
        console.log(value.length);
    } else {
        console.log(value);
    }
}

printValue("Hello");  // Output: 5
printValue(10);       // Output: 10

In this example, the printValue function takes a union argument. By checking the type of value using typeof, TypeScript narrows down the possible types, allowing us to call methods specific to that type.

Union with Arrays

Unions can be used with arrays to allow arrays of mixed types.

union_arrays.ts
let mixedArray: (string | number)[] = ["Hello", 42, "World"];

mixedArray.push(100);
mixedArray.push("TypeScript");

console.log(mixedArray);  // Output: ["Hello", 42, "World", 100, "TypeScript"]

The mixedArray can contain strings or numbers. This is useful for data structures that need to hold heterogeneous values.

Union with Objects

Unions can define objects that can be one of several shapes.

union_objects.ts
type Circle = { kind: "circle"; radius: number };
type Square = { kind: "square"; side: number };

type Shape = Circle | Square;

function getArea(shape: Shape): number {
    if (shape.kind === "circle") {
        return Math.PI * shape.radius ** 2;
    } else {
        return shape.side ** 2;
    }
}

console.log(getArea({ kind: "circle", radius: 5 }));  // Output: 78.53981633974483
console.log(getArea({ kind: "square", side: 4 }));   // Output: 16

This example uses discriminated unions, where the kind property distinguishes between types, allowing safe access to specific properties.

Union with Literals

Unions can be combined with string literals for more precise types.

union_literals.ts
type Direction = "north" | "south" | "east" | "west";

function move(direction: Direction): string {
    return `Moving ${direction}`;
}

console.log(move("north"));  // Output: Moving north
// console.log(move("up"));  // Error: Argument of type '"up"' is not assignable to parameter of type 'Direction'.

The Direction type restricts the possible values to specific strings, providing better type safety.

Union in Function Parameters

Functions can accept union types as parameters.

union_function_params.ts
function formatValue(value: string | number): string {
    if (typeof value === "string") {
        return value.toUpperCase();
    } else {
        return value.toFixed(2);
    }
}

console.log(formatValue("hello"));  // Output: HELLO
console.log(formatValue(3.14159));  // Output: 3.14

The formatValue function handles both strings and numbers, formatting them appropriately based on their type.

Union with Optional Properties

Unions can be used to make properties optional in a type-safe way.

union_optional.ts
type User = { name: string; age?: number | undefined };

let user1: User = { name: "Alice" };
let user2: User = { name: "Bob", age: 30 };

console.log(user1.age);  // Output: undefined
console.log(user2.age);  // Output: 30

The age property can be a number or undefined, allowing for optional properties without using the ? syntax directly in some cases.

Type Operators with Unions

Intersection

The intersection operator (&) combines multiple types into a single type that has all properties of the intersected types.

intersection_operator.ts
interface Person {
    name: string;
    age: number;
}

interface Developer {
    language: string;
}

type Programmer = Person & Developer;

let programmer: Programmer = {
    name: "Jan",
    age: 35,
    language: "TypeScript"
};

console.log(programmer);

In this example, the Programmer type is created by intersecting the Person and Developer interfaces using the & operator. This means a Programmer must have all properties from both Person (name and age) and Developer (language). The programmer object demonstrates this by including all required fields. Intersections are useful when you want a type that combines multiple sets of properties.

Distributive Conditional Types

Distributive conditional types apply a type operation to each member of a union, allowing you to transform or filter each type in the union individually.

distributive_conditional_types.ts
type ElementType<T> = T extends (infer U)[] ? U : never;

type MyUnionArray = number[] | boolean[] | string[];
type MyUnionElement = ElementType<MyUnionArray>; // number | boolean | string

const numberArray: MyUnionArray = [1, 2, 3]; 
const booleanArray: MyUnionArray = [true, false]; 
const stringArray: MyUnionArray = ["a", "b", "c"]; 

console.log(numberArray); 
console.log(booleanArray);
console.log(stringArray);

In this example, the ElementType distributive conditional type extracts the element type from each array type in the MyUnionArray union. As a result, MyUnionElement becomes number | boolean | string. This technique is useful for transforming or extracting information from each member of a union type.

Mapped Types

Mapped types apply a type transformation to each property of an object type.

mapped_types.ts
type ReadOnly<T> = {
    readonly [P in keyof T]: T[P];
};

interface Person {
    name: string;
    age: number;
}

type ReadOnlyPerson = ReadOnly<Person>;

let person: ReadOnlyPerson = {
    name: "Jan",
    age: 35
};

// person.name = "John";  // Error: Cannot assign to 'name' because it is a read-only property.

In this example, the generic ReadOnly<T> mapped type takes any object type T and produces a new type where all properties are marked as readonly. The Person interface is used to create a ReadOnlyPerson type, making both name and age immutable. Attempting to assign a new value to person.name results in a compile-time error, ensuring the object cannot be modified after creation.

Best Practices for Using Unions

Source

TypeScript Unions and Intersections Documentation

In this article, we have explored TypeScript unions and demonstrated their usage through practical examples.

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.