32. Pick, Omit, Exclude
TypeScript: Броня. Урок 31: Pick, Omit, Exclude
Заголовок раздела «TypeScript: Броня. Урок 31: Pick, Omit, Exclude»Pick, Omit и Exclude - это встроенные utility types для выборки или исключения свойств/типов. Они позволяют создавать подмножества существующих типов без дублирования кода, что критически важно для поддержки DRY (Don’t Repeat Yourself) принципа в типизации.
Pick<T, K>
Заголовок раздела «Pick<T, K>»Pick<T, K> выбирает только указанные свойства из типа:
// Определение Pick (встроенное)type Pick<T, K extends keyof T> = { [P in K]: T[P];};
// Пример использованияinterface User { id: string; name: string; email: string; age: number; role: 'admin' | 'user'; createdAt: Date;}
// Выбираем только id и nametype UserPreview = Pick<User, 'id' | 'name'>;// {// id: string;// name: string;// }
// Выбираем несколько полей для публичного APItype PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// Практическое применениеfunction getUserPreview(user: User): UserPreview { return { id: user.id, name: user.name, };}Omit<T, K>
Заголовок раздела «Omit<T, K>»Omit<T, K> исключает указанные свойства из типа:
// Определение Omit (встроенное)type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// Пример использованияinterface User { id: string; name: string; email: string; password: string; salt: string;}
// Исключаем чувствительные данныеtype SafeUser = Omit<User, 'password' | 'salt'>;// {// id: string;// name: string;// email: string;// }
// Практическое применение: Create DTOtype CreateUserDTO = Omit<User, 'id'>;
function createUser(userData: CreateUserDTO): User { return { id: generateId(), ...userData, };}
const newUser = createUser({ name: 'Alice', password: 'hashed', salt: 'random',}); // ✓Exclude<T, U>
Заголовок раздела «Exclude<T, U>»Exclude<T, U> исключает типы из union, которые присваиваемы к U:
// Определение Exclude (встроенное)type Exclude<T, U> = T extends U ? never : T;
// Пример использованияtype AllColors = 'red' | 'green' | 'blue' | 'yellow';
// Исключаем 'yellow'type PrimaryColors = Exclude<AllColors, 'yellow'>;// 'red' | 'green' | 'blue'
// Исключаем несколькоtype WarmColors = Exclude<AllColors, 'blue' | 'green'>;// 'red' | 'yellow'
// С типами объектовtype A = string | number | boolean;type B = Exclude<A, string>; // number | boolean
// Практическое применениеtype HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';type SafeMethod = Exclude<HttpMethod, 'DELETE' | 'PATCH'>;// 'GET' | 'POST' | 'PUT'
function safeFetch(method: SafeMethod, url: string) { // Только безопасные методы}
safeFetch('GET', '/api/users'); // ✓// safeFetch('DELETE', '/api/users'); // ✗ ОшибкаКомбинирование Utility Types
Заголовок раздела «Комбинирование Utility Types»// Pick + Omitinterface Product { id: string; name: string; price: number; description: string; categoryId: string; createdAt: Date; updatedAt: Date;}
// Публичные поля без timestampstype PublicProduct = Omit<Product, 'createdAt' | 'updatedAt'>;
// Только метаданныеtype ProductMetadata = Pick<Product, 'createdAt' | 'updatedAt'>;
// Update DTO - всё кроме id и timestampstype UpdateProductDTO = Omit<Product, 'id' | 'createdAt' | 'updatedAt'>;
// Pick + Partialtype PartialUpdate<T, K extends keyof T> = Partial<Pick<T, K>>;
type ProductPartialUpdate = PartialUpdate<Product, 'name' | 'price' | 'description'>;// {// name?: string;// price?: number;// description?: string;// }Практический пример: API Models
Заголовок раздела «Практический пример: API Models»// Базовая модельinterface BaseEntity { id: string; createdAt: Date; updatedAt: Date; deletedAt: Date | null;}
interface User extends BaseEntity { email: string; username: string; password: string; firstName: string; lastName: string; role: 'admin' | 'user' | 'moderator';}
// CREATE - без id и timestampstype CreateUserInput = Omit<User, keyof BaseEntity>;// {// email: string;// username: string;// password: string;// firstName: string;// lastName: string;// role: 'admin' | 'user' | 'moderator';// }
// UPDATE - без id, timestamps, и password partialtype UpdateUserInput = Partial< Omit<User, keyof BaseEntity | 'password'>> & { password?: string };
// PUBLIC - без password и deletedAttype PublicUser = Omit<User, 'password' | 'deletedAt'>;
// PROFILE - только пользовательские данныеtype UserProfile = Pick<User, 'firstName' | 'lastName' | 'username'>;
// Использованиеclass UserService { async create(input: CreateUserInput): Promise<PublicUser> { const user: User = { id: generateId(), createdAt: new Date(), updatedAt: new Date(), deletedAt: null, ...input, };
await db.save(user);
const { password, deletedAt, ...publicUser } = user; return publicUser; }
async update(id: string, input: UpdateUserInput): Promise<PublicUser> { const user = await db.findById(id);
Object.assign(user, input, { updatedAt: new Date() }); await db.save(user);
const { password, deletedAt, ...publicUser } = user; return publicUser; }
async getProfile(id: string): Promise<UserProfile> { const user = await db.findById(id);
return { firstName: user.firstName, lastName: user.lastName, username: user.username, }; }}Extract (противоположность Exclude)
Заголовок раздела «Extract (противоположность Exclude)»// Extract - выбирает типы, которые присваиваемы к Utype Extract<T, U> = T extends U ? T : never;
type AllTypes = string | number | boolean | null | undefined;
// Извлекаем только primitive typestype Primitives = Extract<AllTypes, string | number | boolean>;// string | number | boolean
// Извлекаем nullable typestype Nullable = Extract<AllTypes, null | undefined>;// null | undefined
// С union объектовtype Shape = | { kind: 'circle'; radius: number } | { kind: 'square'; size: number } | { kind: 'rectangle'; width: number; height: number };
// Извлекаем только circletype Circle = Extract<Shape, { kind: 'circle' }>;// { kind: 'circle'; radius: number }
// Извлекаем типы с определённым полемtype HasSize = Extract<Shape, { size: number }>;// { kind: 'square'; size: number }Жизненный пример: Form Builder
Заголовок раздела «Жизненный пример: Form Builder»// Type-safe form builderinterface FormField { type: 'text' | 'number' | 'email' | 'password' | 'checkbox' | 'select'; label: string; placeholder?: string; required: boolean; validation?: (value: any) => string | null;}
interface TextField extends FormField { type: 'text' | 'email' | 'password'; maxLength?: number;}
interface NumberField extends FormField { type: 'number'; min?: number; max?: number;}
interface SelectField extends FormField { type: 'select'; options: Array<{ label: string; value: string }>;}
type AllFields = TextField | NumberField | SelectField;
// Извлекаем только text-based поляtype TextBasedFields = Extract<AllFields, { type: 'text' | 'email' | 'password' }>;
// Исключаем select поляtype NonSelectFields = Exclude<AllFields, { type: 'select' }>;
// Pick общих свойств для всех полейtype CommonFieldProps = Pick<FormField, 'label' | 'required'>;
// Omit validation для простых полейtype SimpleField = Omit<FormField, 'validation'>;
// Builder classclass FormBuilder<T extends Record<string, any>> { private fields: Map<keyof T, FormField> = new Map();
addField<K extends keyof T>( name: K, field: Omit<FormField, 'label'> & { label?: string } ): this { this.fields.set(name, { ...field, label: field.label ?? String(name), } as FormField);
return this; }
getField<K extends keyof T>(name: K): Pick<FormField, 'label' | 'type'> | undefined { const field = this.fields.get(name); if (!field) return undefined;
return { label: field.label, type: field.type, }; }
build(): Record<keyof T, Omit<FormField, 'validation'>> { const result = {} as Record<keyof T, Omit<FormField, 'validation'>>;
this.fields.forEach((field, name) => { const { validation, ...simpleField } = field; result[name] = simpleField; });
return result; }}
// Использованиеinterface LoginForm { email: string; password: string; rememberMe: boolean;}
const form = new FormBuilder<LoginForm>() .addField('email', { type: 'email', required: true, }) .addField('password', { type: 'password', required: true, }) .addField('rememberMe', { type: 'checkbox', label: 'Remember me', required: false, });
const builtForm = form.build();Advanced Patterns
Заголовок раздела «Advanced Patterns»// Omit multiple базовых типовtype OmitMultiple<T, K extends keyof T, U extends keyof T> = Omit<T, K | U>;
// Pick with renametype Rename<T, K extends keyof T, N extends string> = Omit<T, K> & Record<N, T[K]>;
interface User { id: string; name: string;}
// Переименование id в userIdtype UserWithRenamedId = Rename<User, 'id', 'userId'>;// { name: string; userId: string }
// Conditional Omittype OmitByType<T, ValueType> = { [K in keyof T as T[K] extends ValueType ? never : K]: T[K];};
interface Mixed { name: string; age: number; active: boolean; count: number;}
// Исключаем все number поляtype NoNumbers = OmitByType<Mixed, number>;// { name: string; active: boolean }
// Conditional Picktype PickByType<T, ValueType> = { [K in keyof T as T[K] extends ValueType ? K : never]: T[K];};
// Выбираем только number поляtype OnlyNumbers = PickByType<Mixed, number>;// { age: number; count: number }Ключевые моменты
Заголовок раздела «Ключевые моменты»Pick<T, K>выбирает указанные свойства из типаOmit<T, K>исключает указанные свойства из типаExclude<T, U>исключает типы из unionExtract<T, U>извлекает типы из union (противоположность Exclude)- Можно комбинировать с
Partial,Required,Readonly - Используются для создания DTO, view models, API responses
- Помогают избежать дублирования типов
- Можно создавать custom utility types на их основе
- Критически важны для DRY в типизации
- Широко используются в реальных проектах для управления типами