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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
- Use Union Narrowing: Utilize TypeScript's union narrowing feature to determine the actual type of a union variable.
- Prefer Specific Types: Avoid unions when possible, as they can make code harder to understand and maintain.
- Use Type Operators: Leverage TypeScript's type operators, such as intersection and mapped types, for powerful type manipulation.
- Handle Edge Cases: Always check for edge cases when working with unions, as they can introduce unexpected behavior.
- Use Discriminated Unions: For complex unions, add a discriminant property to safely narrow types.
Source
TypeScript Unions and Intersections Documentation
In this article, we have explored TypeScript unions and demonstrated their usage through practical examples.
Author
List all TypeScript tutorials.