- Published on
Part 4: Type Assertions, Unions, and Intersections
- Authors

- Name
- Diego Herrera Redondo
- @diegxherrera
Welcome back to the TypeScript Crash Course! 🎉 In Part 3, we explored functions and generics, adding flexibility to our code. In Part 4, we’ll dive into more advanced type management techniques: type assertions, unions, and intersections. These features allow us to handle complex data structures, work with multiple types, and take control of specific type behaviors. Let’s get started! 🚀
What are Type Assertions? 🤔
Type assertions allow you to tell TypeScript that a value has a specific type, overriding TypeScript’s inferred type. This is useful in situations where TypeScript doesn’t have enough context to accurately determine a type.
Syntax
There are two ways to use type assertions:
- Angle-bracket syntax:
<Type>value - As-syntax:
value as Type(recommended in modern TypeScript)
Example: Type Assertion
let value: unknown = "Hello, TypeScript";
// Using angle-bracket syntax
let length1 = (<string>value).length;
// Using as-syntax
let length2 = (value as string).length;
console.log(length1); // Output: 17
console.log(length2); // Output: 17
In this example, we assert that value is a string, allowing us to access the length property.
Working with Union Types 🔀
Union types allow a variable to hold values of different types. They’re defined using the | symbol and are helpful for handling scenarios where a value could be of multiple types.
Example: Basic Union Type
let id: string | number;
id = "abc123"; // Valid
id = 123; // Valid
// id = true; // ❌ Error: Type 'boolean' is not assignable to type 'string | number'.
Here, id can be either a string or a number, but nothing else.
Union Types in Functions
Union types work well in functions, where different argument types may require different logic.
function printId(id: string | number): void {
if (typeof id === "string") {
console.log("ID is a string:", id.toUpperCase());
} else {
console.log("ID is a number:", id.toFixed(2));
}
}
printId("abc123"); // Output: ID is a string: ABC123
printId(123.45); // Output: ID is a number: 123.45
In this function, TypeScript narrows down the type using typeof, allowing us to handle string and number differently.
Intersection Types 🔗
Intersection types combine multiple types into one. A variable with an intersection type must satisfy all the included types.
Example: Intersection Type
interface Name {
name: string;
}
interface Age {
age: number;
}
type Person = Name & Age;
let person: Person = {
name: "Alice",
age: 25
};
console.log(person);
In this example, Person has properties from both Name and Age, requiring person to have both name and age.
Combining Union and Intersection Types 🧩
Union and intersection types can be combined for more advanced type management.
Example: Union of Intersections
interface Dog {
type: "dog";
bark: () => void;
}
interface Cat {
type: "cat";
meow: () => void;
}
type Pet = Dog | Cat;
function interactWithPet(pet: Pet) {
if (pet.type === "dog") {
pet.bark();
} else {
pet.meow();
}
}
In this example, Pet is a union of Dog and Cat. We use type to determine the type of pet, allowing us to call the appropriate method.
Practical Example: Advanced Form Validation 📋
Let’s create a function to validate form data using union and intersection types.
- Define an interface
UserFormwithusername(string) andemail(string). - Define an interface
AdminFormwithrole(string) andpermissions(array of strings). - Create a union type
Formthat can be eitherUserFormorAdminForm. - Write a function
validateFormthat validates based on the type.
Example Solution
interface UserForm {
username: string;
email: string;
}
interface AdminForm {
role: string;
permissions: string[];
}
type Form = UserForm | AdminForm;
function validateForm(form: Form): void {
if ("username" in form) {
console.log("Validating user form:", form.username, form.email);
} else if ("role" in form) {
console.log("Validating admin form:", form.role, form.permissions);
}
}
const userForm: UserForm = { username: "Alice", email: "alice@example.com" };
const adminForm: AdminForm = { role: "admin", permissions: ["read", "write"] };
validateForm(userForm); // Output: Validating user form: Alice alice@example.com
validateForm(adminForm); // Output: Validating admin form: admin [ 'read', 'write' ]
In this example, validateForm distinguishes between UserForm and AdminForm based on their properties, allowing for flexible validation.
Practice Challenge: Flexible Shape Calculation 🎲
Let’s practice with a shape calculator.
- Define an interface
SquarewithsideLength(number). - Define an interface
Rectanglewithwidthandheight(both numbers). - Create a union type
Shapethat can be eitherSquareorRectangle. - Write a function
calculateAreathat calculates the area based on the type.
Example Solution
interface Square {
sideLength: number;
}
interface Rectangle {
width: number;
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape): number {
if ("sideLength" in shape) {
return shape.sideLength ** 2;
} else {
return shape.width * shape.height;
}
}
const square: Square = { sideLength: 5 };
const rectangle: Rectangle = { width: 4, height: 6 };
console.log("Square area:", calculateArea(square)); // Output: Square area: 25
console.log("Rectangle area:", calculateArea(rectangle)); // Output: Rectangle area: 24
This example uses a union type to calculate the area based on the shape’s properties.
Wrapping Up
In Part 4 of our TypeScript series, we explored type assertions, union types, and intersection types. These tools give you control over complex type scenarios, allowing for greater flexibility and precision in your TypeScript projects.
In Part 5, we’ll wrap up the TypeScript series by exploring type guards, enums, and advanced type manipulation techniques. Thanks for following along, and happy coding! 🎉