TypeScript offers powerful mechanisms for composing complex types using union and intersection types. This chapter delves into the world of union and intersection types, exploring their functionalities, usage patterns, and how they enhance type safety and code expressiveness in your TypeScript applications.
Union types allow you to define a variable that can hold one of several types. This is useful when a variable can have different values of specific types at different times in your code.
type LoginState = "pending" | "success" | "failed";
let loginStatus: LoginState;
loginStatus = "pending"; // Valid
console.log(loginStatus); // Output: "pending"
loginStatus = "success"; // Also valid
console.log(loginStatus); // Output: "success"
// loginStatus = "invalid"; // Error: "invalid" is not a valid LoginState
The pipe symbol (|
) is used to separate the different types within a union type definition.
type Product = { name: string; price: number } | string;
let product: Product;
product = { name: "Hat", price: 15 }; // Valid (object type)
console.log(product.name); // Output: "Hat"
product = "T-Shirt"; // Also valid (string type)
console.log(product.length); // Error: 'length' does not exist on string type in 'product'
// Consider type guards or checks before accessing properties to handle both types safely
Intersection types allow you to create a new type that incorporates the properties of multiple existing types. This is useful when you need a variable to have characteristics from several different types.
interface User {
name: string;
age: number;
}
interface Address {
street: string;
city: string;
}
type UserWithAddress = User & Address;
const user: UserWithAddress = {
name: "Alice",
age: 30,
street: "Main Street",
city: "New York"
};
console.log(user.name, user.street); // Output: "Alice Main Street"
The ampersand symbol (&
) is used to combine multiple types within an intersection type definition.
type SecuredString = string & { isEncrypted: boolean };
const password: SecuredString = "secret123" as SecuredString; // Type assertion for clarity
// password.isEncrypted; // Error: 'isEncrypted' does not exist on type 'string'
// Use with custom logic (if applicable) to ensure the secured string has the isEncrypted property
function encryptString(str: string): SecuredString {
// ... encryption logic
return { value: str, isEncrypted: true };
}
const encryptedPassword = encryptString(password);
console.log(encryptedPassword.isEncrypted); // Output: true (assuming encryption logic sets it)
Discriminated unions are a special kind of union type where all member types share a common property that uniquely identifies their type. This allows for more robust type safety and conditional logic based on the discriminator property.
interface LoginSuccess {
type: "success";
user: User; // User interface defined elsewhere
}
interface LoginFailed {
type: "failed";
error: string;
}
type LoginResult = LoginSuccess | LoginFailed;
function handleLogin(result: LoginResult) {
if (result.type === "success") {
console.log("Login successful! Welcome,", result.user.name);
} else {
console.error("Login failed:", result.error);
}
}
const successfulLogin: LoginResult = { type: "success", user: { name: "Alice" } };
handleLogin(successfulLogin); // Output: "Login successful! Welcome, Alice"
const failedLogin: LoginResult = { type: "failed", error: "Invalid username or password" };
handleLogin(failedLogin); // Output: "Login failed: Invalid username or password"
Union and intersection types are fundamental tools in TypeScript for defining complex data structures and function behaviors. By understanding their capabilities, limitations, and best practices, you can leverage them effectively to write expressive, type-safe, and well-structured TypeScript code. Remember to choose the appropriate type based on your needs and prioritize code clarity when using unions and intersections. Happy coding !❤️