TypeScript Utility Types — Complete Reference
Advertisement
TypeScript provides a comprehensive set of utility types that transform types into new types. These are essential for reducing boilerplate and expressing complex type relationships clearly. Mastering utility types is a hallmark of advanced TypeScript developers.
Essential Utility Types
Partial and Required
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Make all properties optional
type PartialUser = Partial<User>;
const updateUser: PartialUser = { name: "Alice" }; // Valid
// Make all properties required
type RequiredUser = Required<PartialUser>;
const completeUser: RequiredUser = {
id: 1,
name: "Alice",
email: "alice@example.com",
age: 30,
}; // All required
Pick and Omit
// Select specific properties
type UserPreview = Pick<User, "id" | "name">;
const preview: UserPreview = { id: 1, name: "Alice" };
// Exclude specific properties
type UserWithoutEmail = Omit<User, "email">;
const user: UserWithoutEmail = {
id: 1,
name: "Alice",
age: 30,
};
// Combining Pick and Omit for clarity
type PublicUser = Omit<User, "age">;
type AdminUser = User;
- Essential Utility Types
- Partial and Required
- Pick and Omit
- Record
- Type Extraction Utilities
- Extract, Exclude, and NonNullable
- ReturnType and Parameters
- String Manipulation Utilities
- Advanced Utility Patterns
- Readonly and Mutable
- Getters and Setters
- Flatten Arrays
- Real-World Examples
- API Response Wrapper
- Form State Management
- Event System
- Performance Tips
- FAQ
Record
// Create object with specific keys
type Permissions = Record<"read" | "write" | "delete", boolean>;
const adminPermissions: Permissions = {
read: true,
write: true,
delete: true,
};
// Record with enum keys
enum Role {
Admin = "admin",
User = "user",
Guest = "guest",
}
type RolePermissions = Record<Role, boolean>;
const permissions: RolePermissions = {
[Role.Admin]: true,
[Role.User]: true,
[Role.Guest]: false,
};
// Record with union types
type Environment = "development" | "staging" | "production";
type ConfigByEnv = Record<Environment, { apiUrl: string }>;
const config: ConfigByEnv = {
development: { apiUrl: "http://localhost:3000" },
staging: { apiUrl: "https://staging.example.com" },
production: { apiUrl: "https://api.example.com" },
};
Type Extraction Utilities
Extract, Exclude, and NonNullable
type Union = string | number | boolean;
// Extract matching types
type StringOrNumber = Extract<Union, string | number>; // string | number
type OnlyString = Extract<Union, string>; // string
// Exclude matching types
type NoString = Exclude<Union, string>; // number | boolean
type NoNumber = Exclude<Union, number | boolean>; // string
// Remove null and undefined
type Nullable = string | null | undefined;
type NonNullableString = NonNullable<Nullable>; // string
ReturnType and Parameters
function getUserById(id: number): Promise<User> {
return Promise.resolve({ id, name: "", email: "", age: 0 });
}
type UserResponse = ReturnType<typeof getUserById>; // Promise<User>
type UserParams = Parameters<typeof getUserById>; // [number]
// Extract from constructor
class UserService {
constructor(dbUrl: string, apiKey: string) {}
}
type ServiceParams = ConstructorParameters<typeof UserService>;
// [string, string]
type ServiceInstance = InstanceType<typeof UserService>; // UserService
String Manipulation Utilities
// Capitalize first character
type Capitalized = Capitalize<"hello">; // "Hello"
// Uncapitalize first character
type Uncapitalized = Uncapitalize<"Hello">; // "hello"
// Convert to uppercase
type Uppercase = Uppercase<"hello">; // "HELLO"
// Convert to lowercase
type Lowercase = Lowercase<"HELLO">; // "hello"
Advanced Utility Patterns
Readonly and Mutable
interface User {
readonly id: number;
name: string;
}
// Make all properties readonly
type ReadonlyUser = Readonly<User>;
// {
// readonly id: number;
// readonly name: string;
// }
// Remove readonly modifiers
type MutableUser = {
-readonly [K in keyof User]: User[K];
};
// Practical: deep readonly for API responses
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
Getters and Setters
interface User {
id: number;
name: string;
email: string;
}
// Create getter methods for all properties
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// }
// Create setter methods
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type UserSetters = Setters<User>;
// {
// setId: (value: number) => void;
// setName: (value: string) => void;
// setEmail: (value: string) => void;
// }
Flatten Arrays
// Extract array element type
type Flatten<T> = T extends Array<infer U> ? U : T;
type NestedArray = [1, 2, [3, 4]];
type FlattenedArray = Flatten<NestedArray>; // 1 | 2 | [3, 4]
// Deep flatten
type DeepFlatten<T> = T extends Array<infer U>
? DeepFlatten<U>
: T;
type DeepFlattened = DeepFlatten<[1, [2, [3, 4]]]>; // 1 | 2 | 3 | 4
Real-World Examples
API Response Wrapper
// Create consistent API responses
type ApiResponse<T> = {
status: "success" | "error";
data: T;
timestamp: Date;
errors?: string[];
};
type UserResponse = ApiResponse<User>;
type UsersResponse = ApiResponse<User[]>;
// Create paginated response
type PaginatedApiResponse<T> = ApiResponse<{
items: T[];
total: number;
page: number;
pageSize: number;
}>;
Form State Management
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
// Form values
type FormValues = LoginForm;
// Form errors - optional fields that can be errors
type FormErrors = Partial<Record<keyof LoginForm, string>>;
// Form touched - track which fields user interacted with
type FormTouched = Partial<Record<keyof LoginForm, boolean>>;
interface FormState {
values: FormValues;
errors: FormErrors;
touched: FormTouched;
}
Event System
interface EventMap {
"user:created": { userId: number };
"user:updated": { userId: number; changes: Partial<User> };
"user:deleted": { userId: number };
}
type EventType = keyof EventMap;
type EventPayload<T extends EventType> = EventMap[T];
class EventEmitter {
on<T extends EventType>(
event: T,
callback: (payload: EventPayload<T>) => void
): void {
// Implementation
}
emit<T extends EventType>(event: T, payload: EventPayload<T>): void {
// Implementation
}
}
const emitter = new EventEmitter();
emitter.on("user:created", (payload) => {
// payload: { userId: number }
console.log(payload.userId);
});
Performance Tips
- Avoid complex nested conditionals - they slow compilation
- Use
typeoverinterfacefor utilities - interfaces have more overhead - Cache computed types - TypeScript caches type definitions
FAQ
Q: What's the difference between Pick and Omit? A: Pick includes specific properties, Omit excludes them. Use whichever makes your intent clearer. For large interfaces with few exceptions, Omit is cleaner; otherwise, Pick is more explicit.
Q: Can I create custom utility types? A: Absolutely! Use mapped types and conditional types to build reusable utilities. Many popular libraries like type-fest provide additional utilities beyond the built-in ones.
Q: How does Record differ from an interface? A: Record is type-level and creates a type with specific keys. Interfaces are more flexible and better for documentation. Use Record for known key sets, interfaces for object shapes.
Utility types are the Swiss Army knife of TypeScript. They eliminate boilerplate, enforce consistency, and make complex type relationships explicit. Mastering them separates competent TypeScript developers from exceptional ones.
Advertisement