TypeScript Utility Types — Complete Reference for Backend Devs
Advertisement
Introduction
Why This Matters
TypeScript's built-in utility types eliminate boilerplate and enforce consistency across large backend codebases. Without them, you end up manually copying interface definitions and making small, error-prone modifications. With utility types, you derive new types from existing ones at the type level — no duplication, no drift.
Backend engineers encounter utility types constantly: Partial<T> for PATCH request bodies, Omit<T, 'password'> for safe API responses, Record<string, T> for lookup maps, and ReturnType<F> for inferring service return types without importing extra interfaces. Knowing these deeply makes you dramatically more productive.
Beyond the built-ins, understanding how utility types are constructed internally unlocks the ability to write your own — which is necessary for advanced patterns like deep-partial configs, readonly domain entities, and form state management.
Object Transformation Utilities
Partial and Required
interface UserProfile {
id: string;
username: string;
email: string;
bio: string;
avatarUrl: string;
}
// PATCH endpoint body — all fields optional
type UpdateProfileDto = Partial<UserProfile>;
// Make all fields required (reverses optionals)
type RequiredProfile = Required<UpdateProfileDto>;
// Real-world: partial config with required overrides
function updateProfile(
existing: UserProfile,
updates: Partial<UserProfile>
): UserProfile {
return { ...existing, ...updates };
}Pick and Omit
interface User {
id: string;
name: string;
email: string;
passwordHash: string;
role: 'admin' | 'user';
createdAt: Date;
}
// Safe response — never expose password hash
type PublicUser = Omit<User, 'passwordHash'>;
// Minimal auth token payload
type TokenPayload = Pick<User, 'id' | 'email' | 'role'>;
// Create DTO — id and timestamps set by the server
type CreateUserDto = Omit<User, 'id' | 'createdAt' | 'passwordHash'> & {
password: string;
};
// Usage example
function toPublicUser(user: User): PublicUser {
const { passwordHash, ...publicUser } = user;
return publicUser;
}Readonly
interface Config {
apiUrl: string;
timeout: number;
retries: number;
}
// Prevent accidental mutation
const config: Readonly<Config> = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
};
// config.timeout = 10000; // Error: Cannot assign to 'timeout' because it is a read only property
// Deep readonly for nested objects
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type FrozenConfig = DeepReadonly<{
server: { host: string; port: number };
db: { url: string; pool: number };
}>;Record Type
Record<Keys, Value> creates an object type with a fixed set of keys.
type Environment = 'development' | 'staging' | 'production';
type EnvironmentConfig = Record<Environment, {
apiUrl: string;
logLevel: 'debug' | 'info' | 'warn' | 'error';
}>;
const configs: EnvironmentConfig = {
development: { apiUrl: 'http://localhost:3000', logLevel: 'debug' },
staging: { apiUrl: 'https://staging.api.com', logLevel: 'info' },
production: { apiUrl: 'https://api.com', logLevel: 'warn' },
};
// Runtime lookup map
type UserById = Record<string, User>;
// Permission matrix
type PermissionMap = Record<'read' | 'write' | 'delete' | 'admin', boolean>;
const adminPermissions: PermissionMap = {
read: true,
write: true,
delete: true,
admin: true,
};Type Extraction Utilities
Extract and Exclude
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
// Keep only mutation methods
type MutationMethod = Extract<HttpMethod, 'POST' | 'PUT' | 'PATCH' | 'DELETE'>;
// 'POST' | 'PUT' | 'PATCH' | 'DELETE'
// Remove read-only methods
type WritableMethod = Exclude<HttpMethod, 'GET' | 'OPTIONS'>;
// 'POST' | 'PUT' | 'PATCH' | 'DELETE'
// Practical: filter event types
type EventName = 'user:created' | 'user:deleted' | 'order:created' | 'order:shipped';
type UserEvents = Extract<EventName, `user:${string}`>;
// 'user:created' | 'user:deleted'NonNullable
// Remove null and undefined from a type
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // User
// Common pattern with query results
async function requireUser(id: string): Promise<User> {
const user = await db.users.findById(id);
if (!user) throw new Error(`User ${id} not found`);
return user; // NonNullable here — TypeScript knows it's not null
}Function Introspection Utilities
async function createOrder(
userId: string,
items: Array<{ productId: string; quantity: number }>,
shippingAddress: string
): Promise<{ orderId: string; total: number }> {
// implementation
return { orderId: crypto.randomUUID(), total: 0 };
}
// Infer the return type without importing it separately
type OrderResult = ReturnType<typeof createOrder>; // Promise<{ orderId: string; total: number }>
type OrderResultData = Awaited<ReturnType<typeof createOrder>>; // { orderId: string; total: number }
// Infer parameter types
type OrderParams = Parameters<typeof createOrder>;
// [string, Array<{ productId: string; quantity: number }>, string]
// Extract class constructor parameters
class DatabasePool {
constructor(
public readonly url: string,
public readonly maxConnections: number
) {}
}
type PoolArgs = ConstructorParameters<typeof DatabasePool>; // [string, number]
type PoolInstance = InstanceType<typeof DatabasePool>; // DatabasePoolString Manipulation Utilities
// Useful for code generation and mapped type keys
type EventName = 'userCreated' | 'orderShipped' | 'paymentFailed';
type UpperEvents = Uppercase<EventName>;
// 'USERCREATED' | 'ORDERSHIPPED' | 'PAYMENTFAILED'
// Create on/off handlers from event names
type EventHandlers<T extends string> = {
[K in T as `on${Capitalize<K>}`]: () => void;
};
type Handlers = EventHandlers<'click' | 'submit' | 'focus'>;
// { onClick: () => void; onSubmit: () => void; onFocus: () => void }Real-World Patterns
API Response Wrapper
type ApiSuccess<T> = { status: 'success'; data: T; timestamp: string };
type ApiError = { status: 'error'; message: string; code: string };
type ApiResponse<T> = ApiSuccess<T> | ApiError;
type PaginatedData<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
hasNext: boolean;
};
type PaginatedResponse<T> = ApiResponse<PaginatedData<T>>;Form and Validation State
interface OrderForm {
customerId: string;
items: string[];
notes: string;
rushDelivery: boolean;
}
type FormErrors = Partial<Record<keyof OrderForm, string>>;
type FormTouched = Partial<Record<keyof OrderForm, boolean>>;
interface FormState<T> {
values: T;
errors: Partial<Record<keyof T, string>>;
touched: Partial<Record<keyof T, boolean>>;
isSubmitting: boolean;
}Common Mistakes
- Using
Partial<T>whenPickorOmitwould be more precise and intention-revealing - Forgetting
Awaited<>when extracting return types of async functions - Nesting
Partialrecursively by hand instead of writing aDeepPartialutility - Overusing
Record<string, any>— prefer specific key unions for correctness - Ignoring
NonNullableand using manual| nullexclusions throughout the codebase
Best Practices
- Derive types from a single source of truth — never duplicate interface definitions
- Use
Omit<T, 'sensitiveField'>on database entities before sending API responses - Apply
Readonly<T>to configuration objects injected as dependencies - Prefer
ReturnType<typeof fn>over manually typing function return shapes - Build a shared
types/module and import utilities rather than redeclaring them per file
Key Takeaways
Partial<T>makes all fields optional — perfect for PATCH request DTOsRequired<T>is the inverse ofPartialand forces all optional fields to be presentPick<T, Keys>andOmit<T, Keys>derive safe subsets without manual redeclarationRecord<K, V>creates typed lookup maps with exhaustive key checkingExtract<T, U>andExclude<T, U>filter union members based on assignabilityReturnType<F>andParameters<F>introspect function types at compile timeAwaited<T>unwraps nested Promise types introduced by async functionsReadonly<T>and customDeepReadonly<T>prevent accidental mutation of configuration and domain objects
Advertisement