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
Explain how Partial, Pick, Omit, and Record utility types work. When would you use conditional types?
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