Handling complex types: Union, Intersection and Typeguards in TypeScript
published: 11/29/2023
When it comes to building robust applications in TypeScript, handling complex types quickly becomes indispensable.
An in-depth understanding of the notions of unions, intersections and typeguards becomes a major asset for developers looking to enhance the safety of their code.
Introduction
Complex types, such as unions and intersections, represent an advanced set of possibilities for structuring data in TypeScript.
Unions enable the creation of types that can contain several different types, while intersections allow several types to be combined to form a single type.
Typeguards help ensure code security by validating data at program runtime.
Unions and Intersections
Explaining unions and their use
Unions offer remarkable flexibility in defining types capable of representing multiple shapes. For example:
type ID = string | number;
let userId: ID;
userId = "abc123"; // OK
userId = 456; // OK
userId = true; // Error: Type 'boolean' is not assignable to type 'string | number'
Learn more about intersections and how to use them effectively
Unlike unions, intersections allow you to merge types to create a new type with all the characteristics of its components:
interface Car {
brand: string;
color: string;
}
interface Electric {
batteryLife: number;
}
type ElectricCar = Car & Electric;
let myCar: ElectricCar;
myCar = {
brand: "Tesla",
color: "Red",
batteryLife: 300,
};
Comparing use cases to choose between unions and intersections
The decision to use unions or intersections often depends on context and application logic.
Unions are ideal for representing a value that can be of several distinct types, while intersections are better suited to combining types to create a complete new type.
Typeguards: guardians of code safety
Typeguards are functions that check the type of a variable at runtime.
They guarantee greater security and precision in data processing.
Concrete examples of how typeguards can be used to secure code
In this example, we want to calculate the area of a shape. And the calculation changes according to the shape used. Thanks to the in
keyword, we can check for the presence of a property exclusive to our shape and use the right formula.
interface Square {
size: number;
}
interface Rectangle {
width: number;
height: number;
}
interface Circle {
radius: number
}
type Shape = Square | Rectangle | Circle;
function calculateArea(shape: Shape): number {
if ("size" in shape) return shape.size ** 2; // Calculating area of a square
if ("radius" in shape) return Math.PI * shape.radius ** 2 // Calculating area for a circle
return shape.width * shape.height; // Calculating area for a rectangle
In this example, we want to check whether our pet is a dog or a cat and display its information. If we can’t identify it, we’ll throw an exception.
interface Dog extends Animal {
breed: string;
}
interface Cat extends Animal {
color: string;
}
function isDog(animal: any): animal is Dog {
return animal && animal.breed !== undefined;
}
function isCat(animal: any): animal is Cat {
return animal && animal.color !== undefined;
}
function processAnimal(animal: Animal) {
if (isDog(animal))
return console.log(`Dog: ${animal.name}, Breed: ${animal.breed}`);
if (isCat(animal))
return console.log(`Cat: ${animal.name}, Color: ${animal.color}`);
throw new Error("Ouch, this animal is unknown");
}
const dog: Dog = {
name: "Buddy",
breed: "Golden Retriever",
};
const cat: Cat = {
name: "Whiskers",
color: "Gray",
};
processAnimal(dog); // Output: Dog: Buddy, Breed: Golden Retriever
processAnimal(cat); // Output: Cat: Whiskers, Color: Gray
Best practices and tips for optimizing the use of typeguards
Naming typeguard functions: Give typeguard functions clear, explicit names to make them easier to understand and improve code readability.
function isManager(employee: Employee): employee is Manager {
return (employee as Manager).department !== undefined;
}
Use as
or in
with care: Use as
and in
judiciously and precisely to avoid unnecessary conversions or checks that could alter the safety of the code.
if ("department" in employee) {
// Do something...
}
Combine typeguards: Use several typeguards in combination for more complex checks.
function isSeniorManager(employee: Employee): boolean {
return isManager(employee) && employee.department === "Engineering";
}
Typeguards extension: Extend the functionality of typeguards for more specific cases or additional conditions.
function isSeniorManager(employee: Employee): boolean {
return isManager(employee) && employee.department === "Engineering";
}
Avoid code redundancy: Reuse existing typeguards to avoid duplicating similar checks.
function isEmployeeSenior(employee: Employee): boolean {
return isManager(employee) || isSeniorManager(employee);
}
By following these best practices and tips, you can maximize the efficiency and clarity of your typeguards, reinforcing the safety and reliability of your TypeScript code.
Advanced use cases
Managing states in an application
In this example, we use union for the types of the various states and typeguards to check that the data is present before displaying it.
In this way, we can anticipate the contents of the state
variable and the behavior of the handleState
function.
type LoadingState = {
loading: true;
};
type SuccessState<T> = {
loading: false;
data: T;
};
type ErrorState = {
loading: false;
error: string;
};
type State<T> = LoadingState | SuccessState<T> | ErrorState;
function handleState<T>(state: State<T>) {
if (state.loading) {
// Display loading
} else if ("data" in state) {
// Use state.data
} else {
// Display error : state.error
}
}
In this example, we use the combination of typeguards and an intersection. This will allow you to check the type of the box
element to ensure that it contains the properties required for specific processing. In this way, errors are avoided.
type BoxTypes =
| ImageBox
| StaticTextBox
| TagTextBox
| SocialMediaBox
| TagImageBox
| GroupBox;
function isTagBox(box: BoxTypes): box is TagTextBox | TagImageBox {
return isTagTextBox(box) || isTagImageBox(box);
}
Conclusion
Advanced handling of complex types such as unions, intersections and typeguards opens up a world of possibilities for TypeScript developers.
By understanding these concepts and applying them judiciously, you can enhance the robustness and safety of your code, while simplifying the management of complex data structures.
By exploring unions and intersections in depth, mastering typeguards to secure your typing operations, and applying this knowledge in real-world scenarios, you’re armed to take your TypeScript development to new heights.
Keep exploring these concepts, experiment with them in your projects and discover how they can fundamentally transform your approach to software development.