Build bigger types from smaller ones — type aliases, union types, intersection types, and the keyof operator for type-safe property access.
Why: type gives any type a name — object shapes, unions, function signatures. Aliases keep annotations short and give the domain a vocabulary.
// Name any type and reuse it everywhere
type UserID = number;
type Point = { x: number; y: number };
type Callback = (data: string) => void;
function distance(a: Point, b: Point): number {
return Math.hypot(a.x - b.x, a.y - b.y);
}
const onDone: Callback = (data) => console.log('got', data);
const id: UserID = 7;
console.log(id, distance({ x: 0, y: 0 }, { x: 3, y: 4 })); // 7 5
onDone('payload');Why: a union (A | B) is a value that can be one of several types. You can only use members common to all of them — until you narrow. Unions of literals are how you model a fixed set of options.
// One of several types
type Id = number | string;
function normalizeId(id: Id): string {
// must narrow before using type-specific methods
return typeof id === 'number' ? id.toString() : id.trim();
}
// Literal unions — a fixed menu of allowed values
type Theme = 'light' | 'dark' | 'system';
function setTheme(theme: Theme) {
console.log('theme is now', theme);
}
setTheme('dark'); // ok
// setTheme('blue'); // Error: '"blue"' is not assignable to Theme
console.log(normalizeId(42), normalizeId(' abc '));Why: an intersection (A & B) merges shapes — the value must satisfy all of them at once. Use it to compose small reusable pieces into bigger types.
type Timestamps = { createdAt: Date; updatedAt: Date };
type SoftDelete = { deletedAt: Date | null };
// The result must have EVERYTHING
type DbRecord = { id: number } & Timestamps & SoftDelete;
const row: DbRecord = {
id: 1,
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
};
// Common pattern: bolt extra capabilities onto any type
type WithAuthor<T> = T & { author: string };
const post: WithAuthor<{ title: string }> = {
title: 'Hello',
author: 'Alice',
};
console.log(row.id, post.author);Why: keyof turns a type's property names into a union of literals. Combined with generics it lets you write functions that only accept real keys — typos become compile errors.
type User = { id: number; name: string; email: string };
// Union of the property names
type UserKey = keyof User; // 'id' | 'name' | 'email'
// The classic use: a type-safe property getter
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 1, name: 'Alice', email: 'a@x.dev' };
const name = getProp(user, 'name'); // type: string
const id = getProp(user, 'id'); // type: number
// getProp(user, 'age'); // Error: '"age"' is not a key of User
console.log(name, id);