Combine types in powerful ways with union and intersection types.
Union Types
A value can be one of several types. Use the | operator.
type StringOrNumber = string | number;
function processValue(value: StringOrNumber) {
// value can be either string or number
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(2));
}
}
Common Union Patterns
// Multiple primitives
type ID = string | number;
// With null/undefined
type MaybeString = string | null | undefined;
// With specific values
type Status = "pending" | "approved" | "rejected";
// Complex types
type Result = { success: true; data: any } | { success: false; error: string };
Discriminated Unions
Use a common property to distinguish between union members.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
Intersection Types
A value must satisfy all types. Use the & operator.
interface Person {
name: string;
age: number;
}
interface Employee {
employeeId: string;
department: string;
}
type Staff = Person & Employee;
const staff: Staff = {
name: "Alice",
age: 30,
employeeId: "E001",
department: "Engineering"
// Must have all properties from both interfaces
};
Merging Types
type A = { a: number };
type B = { b: string };
type C = A & B; // { a: number; b: string }
Intersection with Primitives
// Intersection of incompatible types results in 'never'
type Impossible = string & number; // never
Combining Union and Intersection
type A = { a: number } | { b: string };
type B = { c: boolean } & A;
// B is: ({ c: boolean; a: number }) | ({ c: boolean; b: string })
Type Narrowing
TypeScript narrows types based on control flow.
function process(value: string | number) {
if (typeof value === "string") {
// TypeScript knows value is string here
console.log(value.length);
} else {
// TypeScript knows value is number here
console.log(value.toFixed(2));
}
}
Type Guards
interface Dog {
type: "dog";
bark(): void;
}
interface Cat {
type: "cat";
meow(): void;
}
type Pet = Dog | Cat;
function isDog(pet: Pet): pet is Dog {
return pet.type === "dog";
}
function makeSound(pet: Pet) {
if (isDog(pet)) {
pet.bark(); // TypeScript knows it's a Dog
} else {
pet.meow(); // TypeScript knows it's a Cat
}
}
Exercises
Exercise 1: Union Types
// Create a type 'ID' that can be string or number
// Create a function that accepts ID and returns a string representation
Exercise 2: Discriminated Union
// Create a discriminated union for API responses:
// - Success: { status: "success"; data: T }
// - Error: { status: "error"; message: string }
// Create a function that handles both cases
Exercise 3: Intersection Types
// Create two interfaces:
// - 'Readable' with read(): string
// - 'Writable' with write(data: string): void
// Create an intersection type 'ReadWrite' that has both
Exercise 4: Type Guards
// Create a type guard function 'isString' that:
// - Takes value: unknown
// - Returns value is string
// Use it to safely process a value
Exercise 5: Complex Union
// Create a union type for different event types:
// - ClickEvent: { type: "click"; x: number; y: number }
// - KeyEvent: { type: "key"; key: string }
// - MouseEvent: { type: "mouse"; button: number }
// Create a handler function for all event types
Best Practices
- Use discriminated unions - Makes type narrowing easier
- Prefer union over any - More type-safe
- Use type guards - Better than type assertions
- Name union types clearly - Makes code self-documenting
- Use intersection for composition - Combine multiple types
Common Mistakes
- Not narrowing union types
// ❌ Bad
function process(value: string | number) {
value.toUpperCase(); // Error: Property doesn't exist on type 'number'
}
// ✅ Good
function process(value: string | number) {
if (typeof value === "string") {
value.toUpperCase();
}
}
- Intersecting incompatible types
// ❌ Results in 'never'
type Bad = string & number;
// ✅ Use union instead
type Good = string | number;
- Forgetting discriminated property
// ❌ Hard to narrow
type Shape = { radius: number } | { width: number; height: number };
// ✅ Easy to narrow
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number };
Next Steps
Congratulations! You've completed the Beginner level.
Proceed to Intermediate Level to learn about Generics, Utility Types, and more advanced concepts.