Union types in TypeScript allow a variable or a function to accept multiple data types. This gives us flexibility to define types and helps us in situations where values can be more than one type.
TypeScript still performs type checks at compile time, so we retain most of the advantages of static typing.
Union Type Declaration
To declare a union type, the vertical bar operator (|
) is used between the types to be combined. This indicates that a variable can be of one type or another.
let value: number | string;
value = 42; // Correct
value = "text"; // Correct
value = true; // ❌ Error: Type 'boolean' cannot be assigned to type 'number | string'.
In the example above, the variable value
can contain a number or a string, but not a boolean value.
Using union types in functions
Functions can also use union types for their parameters and return values.
function printValue(value: number | string): void {
console.log(value);
}
printValue(123); // Correct
printValue("string"); // Correct
printValue(true); // ❌ Error: Type 'boolean' cannot be assigned to type 'number | string'.
Union types in object properties
We can use union types in the properties of interfaces and types to provide flexibility for the properties within an object.
interface Product {
id: number;
name: string;
price: number | string; // The price property can be a number or a string
}
// product1 has number as price
let product1: Product = {
id: 1,
name: "Product A",
price: 100
};
// product2 has string as price
let product2: Product = {
id: 2,
name: "Product B",
price: "One hundred dollars"
};
In this example, the price
property of the Product
object can be a number or a string.
Narrowing
When working with union types, it is common to determine the specific type of a variable at runtime.
This is known as “narrowing” and can be achieved using type checks (type guards
)
Using typeof
The typeof
operator is useful for narrowing types when working with primitive types like numbers and strings.
function processValue(value: number | string): void {
// Check if the value is a number
if (typeof value === "number") {
console.log(`The value is a number: ${value}`);
} else {
// If it is not a number, it must be a string
console.log(`The value is a string: ${value}`);
}
}
processValue(123); // The value is a number: 123
processValue("text"); // The value is a string: text
Using instanceof
The instanceof
operator is used to check if an object is an instance of a specific class, which is useful for narrowing types in classes and complex objects.
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat): void {
// Check if the animal is an instance of Dog
if (animal instanceof Dog) {
animal.bark(); // If it is a Dog, bark
} else {
animal.meow(); // If not a Dog, it must be a Cat and meow
}
}
let myDog = new Dog();
let myCat = new Cat();
makeSound(myDog); // Woof!
makeSound(myCat); // Meow!
Custom Checks
It is also possible to define custom type checks (type predicates
) for more precise type narrowing.
// Bird interface
interface Bird {
fly(): void;
feathers: number;
}
// Fish interface
interface Fish {
swim(): void;
fins: number;
}
// Function that determines if an animal is a Bird
function isBird(animal: Bird | Fish): animal is Bird {
// Check if the 'fly' method is defined on the object
return (animal as Bird).fly !== undefined;
}
function doSomething(animal: Bird | Fish): void {
if (isBird(animal)) {
animal.fly(); // If it is a Bird, fly
} else {
animal.swim(); // If not a Bird, it must be a Fish and swim
}
}
Using Aliases with Union Types
You can use type aliases to make union types clearer and more concise.
type ID = number | string;
function getUser(id: ID): void {
// Logic to get user
}
In this example, we create an alias ID
that can be number
or string
.
Compound type unions
Union types can discriminate even when the types are part of other types or objects.
type ApiResponse = { data: any; error: null } | { data: null; error: string };
function handleResponse(response: ApiResponse): void {
if (response.error) {
console.error(`Error: ${response.error}`);
} else {
console.log(`Data: ${response.data}`);
}
}
let response: ApiResponse = { data: { id: 1, name: "John" }, error: null };
handleResponse(response); // Data: { id: 1, name: "John" }
In this example,
ApiResponse
can be a response with data and no errors, or without data and with errors.- The code handles both possibilities appropriately.