Union Types
Union types allow a value to be one of several types:
// Basic union type
let id: string | number;
id = "abc123"; // OK
id = 123; // OK
// id = true; // Error!
// Union in function parameters
function printId(id: string | number): void {
console.log("ID:", id);
}
// Union with arrays
let mixedArray: (string | number)[] = [1, "two", 3, "four"];
// Union with literal types
type Direction = "north" | "south" | "east" | "west";
let heading: Direction = "north";
// Union with null/undefined
type MaybeString = string | null | undefined;
function processName(name: string | null): string {
if (name === null) {
return "Anonymous";
}
return name.toUpperCase();
}
Type Narrowing
Narrow union types to specific types using type guards:
// typeof narrowing
function padLeft(value: string, padding: string | number): string {
if (typeof padding === "number") {
// padding is number here
return " ".repeat(padding) + value;
}
// padding is string here
return padding + value;
}
// truthiness narrowing
function processValue(value: string | null | undefined): string {
if (value) {
// value is string here (truthy)
return value.toUpperCase();
}
return "default";
}
// equality narrowing
function compare(a: string | number, b: string | boolean): void {
if (a === b) {
// a and b are both string here
console.log(a.toUpperCase());
}
}
// in operator narrowing
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird): void {
if ("swim" in animal) {
animal.swim(); // animal is Fish
} else {
animal.fly(); // animal is Bird
}
}
// instanceof narrowing
function logDate(date: Date | string): void {
if (date instanceof Date) {
console.log(date.toISOString()); // date is Date
} else {
console.log(date); // date is string
}
}
Discriminated Unions
Use a common property to discriminate between union members:
// Discriminated union with 'kind' property
interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function getArea(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;
}
}
// Exhaustiveness checking
function assertNever(x: never): never {
throw new Error("Unexpected value: " + x);
}
function getAreaExhaustive(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;
default:
return assertNever(shape); // Error if case missing
}
}
Intersection Types
Intersection types combine multiple types into one:
// Basic intersection
type Person = {
name: string;
age: number;
};
type Employee = {
employeeId: number;
department: string;
};
type Staff = Person & Employee;
const staff: Staff = {
name: "Alice",
age: 30,
employeeId: 12345,
department: "Engineering"
};
// Intersection with interfaces
interface Timestamps {
createdAt: Date;
updatedAt: Date;
}
interface Identifiable {
id: string;
}
type Entity<T> = T & Timestamps & Identifiable;
type UserEntity = Entity<{ name: string; email: string }>;
const user: UserEntity = {
id: "user-1",
name: "Bob",
email: "bob@example.com",
createdAt: new Date(),
updatedAt: new Date()
};
User-Defined Type Guards
// Type predicate function
interface Cat {
meow(): void;
purr(): void;
}
interface Dog {
bark(): void;
wagTail(): void;
}
// Type guard with 'is' keyword
function isCat(animal: Cat | Dog): animal is Cat {
return "meow" in animal;
}
function interact(animal: Cat | Dog): void {
if (isCat(animal)) {
animal.meow(); // animal is Cat
animal.purr();
} else {
animal.bark(); // animal is Dog
animal.wagTail();
}
}
// Type guard for API response
interface SuccessResponse {
success: true;
data: unknown;
}
interface ErrorResponse {
success: false;
error: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function isSuccess(response: ApiResponse): response is SuccessResponse {
return response.success === true;
}
function handleResponse(response: ApiResponse): void {
if (isSuccess(response)) {
console.log("Data:", response.data);
} else {
console.error("Error:", response.error);
}
}
Practical Examples
// Redux action types
type LoadUsersAction = {
type: "LOAD_USERS";
};
type AddUserAction = {
type: "ADD_USER";
payload: { name: string; email: string };
};
type RemoveUserAction = {
type: "REMOVE_USER";
payload: { id: number };
};
type UserAction = LoadUsersAction | AddUserAction | RemoveUserAction;
function userReducer(state: User[], action: UserAction): User[] {
switch (action.type) {
case "LOAD_USERS":
return state;
case "ADD_USER":
return [...state, { id: Date.now(), ...action.payload }];
case "REMOVE_USER":
return state.filter(u => u.id !== action.payload.id);
}
}
// React component props with variants
type ButtonProps = {
label: string;
onClick: () => void;
} & (
| { variant: "primary"; icon?: never }
| { variant: "icon"; icon: string }
);
// Usage:
//
Key Takeaways
- • Union types (
|) allow one of several types - • Intersection types (
&) combine types - • Use type guards to narrow union types
- • Discriminated unions use a common property
- • Custom type guards use
iskeyword - • Exhaustiveness checking catches missing cases