Explain how Partial, Pick, Omit, and Record utility types work. When would you use conditional types?
6 minadvancedtypescriptpartialpickomitrecord
Quick Answer
TypeScript provides several built-in utility types that help transform existing types. These are essential for creating flexible and reusable type definitions.
Detailed Answer
TypeScript provides several built-in utility types that help transform existing types. These are essential for creating flexible and reusable type definitions.
Core Utility Types:
- Partial<T> - Makes all properties optional:
interface User {
id: number;
name: string;
email: string;
age: number;
}
type PartialUser = Partial<User>;
// Equivalent to:
// {
// id?: number;
// name?: string;
// email?: string;
// age?: number;
// }
// Usage in update functions
function updateUser(id: number, updates: Partial<User>): void {
// Only some fields need to be provided
console.log(`Updating user ${id} with:`, updates);
}
updateUser(1, { name: 'John' }); // OK - only name provided
updateUser(1, { name: 'John', age: 30 }); // OK - multiple fields
- Pick<T, K> - Selects specific properties:
type UserSummary = Pick<User, 'id' | 'name'>;
// Equivalent to:
// {
// id: number;
// name: string;
// }
// Usage for API responses
function getUserSummary(id: number): UserSummary {
const user = getUserById(id);
return {
id: user.id,
name: user.name
};
}
- Omit<T, K> - Excludes specific properties:
type UserWithoutId = Omit<User, 'id'>;
// Equivalent to:
// {
// name: string;
// email: string;
// age: number;
// }
// Usage for creating new users (without ID)
function createUser(userData: Omit<User, 'id'>): User {
return {
id: generateId(),
...userData
};
}
- Record<K, V> - Creates object type with specific keys and values:
type Status = 'loading' | 'success' | 'error';
type StatusMessages = Record<Status, string>;
// Equivalent to:
// {
// loading: string;
// success: string;
// error: string;
// }
const messages: StatusMessages = {
loading: 'Please wait...',
success: 'Operation completed!',
error: 'Something went wrong!'
};
// Dynamic object creation
type UserRoles = Record<string, string[]>;
const userRoles: UserRoles = {
admin: ['read', 'write', 'delete'],
user: ['read'],
guest: []
};
Advanced Utility Types:
- Required<T> - Makes all properties required:
interface Config {
apiUrl?: string;
timeout?: number;
retries?: number;
}
type RequiredConfig = Required<Config>;
// All properties are now required
- Readonly<T> - Makes all properties readonly:
type ReadonlyUser = Readonly<User>;
// All properties are now readonly
- Exclude<T, U> - Excludes types from union:
type AllColors = 'red' | 'green' | 'blue' | 'yellow';
type PrimaryColors = Exclude<AllColors, 'yellow'>; // 'red' | 'green' | 'blue'
- Extract<T, U> - Extracts types from union:
type MixedTypes = string | number | boolean | Date;
type PrimitiveTypes = Extract<MixedTypes, string | number | boolean>;
// 'string' | 'number' | 'boolean'
Custom Utility Types:
// Deep partial - makes nested properties optional too
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Non-nullable - removes null and undefined
type NonNullable<T> = T extends null | undefined ? never : T;
// Function properties only
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
// Optional properties only
type OptionalPropertyNames<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
Conditional Types:
Conditional types allow you to create types that depend on other types. They use the syntax T extends U ? X : Y.
When to Use Conditional Types:
- API Response Handling:
type ApiResponse<T> = T extends string
? { message: T }
: { data: T };
type StringResponse = ApiResponse<string>; // { message: string }
type ObjectResponse = ApiResponse<User>; // { data: User }
- Function Overloading:
type OverloadedFunction<T> = T extends string
? (input: T) => string
: T extends number
? (input: T) => number
: (input: T) => T;
const process: OverloadedFunction<string> = (input) => input.toUpperCase();
const processNumber: OverloadedFunction<number> = (input) => input * 2;
- Array vs Non-Array Handling:
type Flatten<T> = T extends (infer U)[] ? U : T;
type StringArray = Flatten<string[]>; // string
type StringType = Flatten<string>; // string
- Infer Keyword - Extract Types:
// Extract return type from function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Extract parameter types
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// Extract array element type
type ArrayElement<T> = T extends (infer U)[] ? U : never;
// Usage
type MyFunction = (a: string, b: number) => boolean;
type MyReturnType = ReturnType<MyFunction>; // boolean
type MyParameters = Parameters<MyFunction>; // [string, number]
- Recursive Conditional Types:
// Deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// Deep required
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};
// JSON serializable types
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
type JSONSerializable<T> = T extends JSONValue ? T : never;
Practical Examples:
// Form handling with partial updates
interface UserForm {
name: string;
email: string;
age: number;
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
type UserFormUpdate = DeepPartial<UserForm>;
function updateUserForm(updates: UserFormUpdate): void {
// Can update any nested property
console.log('Updating form with:', updates);
}
updateUserForm({
name: 'John',
preferences: {
theme: 'dark'
// notifications can be omitted
}
});
// Event handler types
type EventMap = {
click: MouseEvent;
keydown: KeyboardEvent;
load: Event;
};
type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void;
function addEventListener<T extends keyof EventMap>(
event: T,
handler: EventHandler<T>
): void {
// Type-safe event handling
}
addEventListener('click', (event) => {
// event is typed as MouseEvent
console.log(event.clientX, event.clientY);
});
addEventListener('keydown', (event) => {
// event is typed as KeyboardEvent
console.log(event.key, event.code);
});
Best Practices:
- Use utility types to create variations of existing types
- Combine utility types for complex transformations
- Use conditional types for type-dependent logic
- Leverage
inferto extract types from complex structures - Create custom utility types for domain-specific needs
- Use conditional types sparingly - they can make code hard to understand