Difficulty: Advanced
Once you understand the built-in utility types, you can combine and extend them to build custom utilities tailored to your application's needs. Custom utility types follow the same patterns: mapped types for transforming object properties, conditional types for type-level logic, template literal types for string manipulation, and recursive types for nested structures.
A common pattern is composing built-in utilities to create application-specific types. For example, `DeepReadonly<T>` recursively applies `Readonly` to all nested objects. `PartialBy<T, K>` makes only specific keys optional while keeping the rest required. `Mutable<T>` removes `readonly` from all properties. These composites solve real problems that the built-in utilities alone cannot.
Recursive utility types are powerful but should be used with care. TypeScript has a recursion depth limit (usually around 100 levels), and deeply recursive types can slow down the compiler. Practical recursive types include `DeepPartial<T>` (makes all nested properties optional), `DeepReadonly<T>` (makes all nested properties readonly), and `Flatten<T>` (flattens nested object types).
When building custom utilities, start with the simplest version that works and add complexity only as needed. Document your utility types well, as they can be difficult to read. Consider whether a simpler approach (like explicit interfaces) would be more maintainable. Utility types shine when they eliminate large amounts of duplication, but overusing them can make a codebase harder to understand.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends Function
? T[K]
: DeepPartial<T[K]>
: T[K];
};
interface Config {
db: { host: string; port: number };
cache: { ttl: number; enabled: boolean };
}
const config: DeepReadonly<Config> = {
db: { host: "localhost", port: 5432 },
cache: { ttl: 3600, enabled: true }
};
// config.db.host = "remote"; // Error: readonly
console.log(config.db.host);
console.log(config.cache.ttl);
const partial: DeepPartial<Config> = { db: { host: "remote" } };
console.log(partial.db?.host);
DeepReadonly recursively makes all properties readonly, including nested objects. DeepPartial recursively makes all properties optional. Both skip Function types to preserve method signatures.
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface User {
id: string;
name: string;
email?: string;
phone?: string;
}
// Only id is optional for creation
type CreateUser = PartialBy<User, "id">;
// Email is required for verified users
type VerifiedUser = RequiredBy<User, "email">;
function createUser(data: CreateUser): User {
return {
id: data.id || Math.random().toString(36).slice(2, 8),
name: data.name,
email: data.email,
phone: data.phone
};
}
const user = createUser({ name: "Alice", email: "alice@test.com" });
console.log(`${user.name}: ${user.email}`);
const user2 = createUser({ id: "custom-id", name: "Bob" });
console.log(`${user2.id}: ${user2.name}`);
PartialBy makes specific keys optional by combining Omit and Partial<Pick>. RequiredBy does the inverse. These are more precise than blanket Partial or Required.
type ApiResponse<T> = {
data: T;
status: number;
timestamp: string;
};
type Paginated<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
};
type PaginatedResponse<T> = ApiResponse<Paginated<T>>;
interface Product {
id: string;
name: string;
price: number;
}
function createResponse<T>(data: T): ApiResponse<T> {
return { data, status: 200, timestamp: new Date().toISOString() };
}
const response: PaginatedResponse<Product> = createResponse({
items: [
{ id: "p1", name: "Widget", price: 9.99 },
{ id: "p2", name: "Gadget", price: 19.99 }
],
total: 50,
page: 1,
pageSize: 10
});
console.log(`Page ${response.data.page}: ${response.data.items.length} items`);
console.log(`Total: ${response.data.total}, Status: ${response.status}`);
PaginatedResponse composes ApiResponse and Paginated generics to create a precise type for paginated API endpoints. Each utility type handles one concern.
custom utility types, combining utilities, recursive types, generic constraints, type-level programming