Утиліта type omit в TypeScript
Omit<T, K> - утилітний тип, який бере тип T і повертає його копію без ключів, перелічених у K.
Теорія
TL;DR
- Уяви форму, де певні поля замальовані маркером: решта залишається, тільки вказані поля зникають
- Головна різниця від
Pick:Omitвіднімає ключі від повного типу;Pickбудує новий тип, вибираючи тільки те, що ти назвав - Правило вибору: виключень менше, ніж залишків? Бери
Omit. Потрібна мала підмножина? БериPick - Жодного впливу на рантайм: стирається під час компіляції у JavaScript
- Працює тільки на верхньому рівні ключів; шляхи на кшталт
"address.zip"не мають жодного ефекту
Швидкий приклад
interface User {
id: string;
name: string;
email: string;
password: string;
}
type PublicUser = Omit<User, "password">;
// Результат: { id: string; name: string; email: string }
// password зник, решта без змінДва рядки коду типів, одне прибране поле. Ось і вся ідея.
Ключова різниця
Pick<T, K> будує тип з нуля, вибираючи тільки ті ключі, що ти перелічив. Omit<T, K> стартує від повного типу і видаляє перелічені ключі, зберігаючи все інше. Якщо тип має 10 властивостей і тобі потрібні 9 з них, Omit рятує від необхідності перераховувати всі 9 у Pick.
Коли використовувати
- API-відповідь без чутливих полів:
Omit<User, "password" | "internalId"> - Props React-компонента без внутрішніх callback-ів:
Omit<ComponentProps, "onInternalClick"> - Сутність бази даних, перетворена на DTO:
Omit<Entity, "createdBy" | "updatedAt"> - Тип для форми створення запису:
Omit<Product, "id" | "createdAt">
Порівняння Omit vs Pick
| Утиліта | Що робить | Коли підходить |
|---|---|---|
Pick<T, K> | Залишає тільки названі ключі | Мала підмножина великого типу (2-3 поля) |
Omit<T, K> | Видаляє названі ключі, решту залишає | Потрібна більшість полів, виключити треба кілька |
| Правило вибору | Виключень менше, ніж залишків? Omit. Інакше Pick |
Ці два типи можуть давати однаковий результат:
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
}
type A = Pick<User, "id" | "name" | "email">;
type B = Omit<User, "password" | "createdAt">;
// A і B структурно ідентичніОбирай той варіант, який краще передає намір у конкретному місці коду.
Як компілятор обробляє Omit
TypeScript розгортає Omit<T, K> через Pick і Exclude:
// Вбудоване визначення TypeScript:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// Крок за кроком для Omit<User, "password">:
// 1. keyof User = "id" | "name" | "email" | "password" | "createdAt"
// 2. Exclude<keyof User, "password"> = "id" | "name" | "email" | "createdAt"
// 3. Pick<User, "id" | "name" | "email" | "createdAt"> = результатЖодного впливу на рантайм. Після компіляції у JavaScript від цього нічого не залишається. Ще один нюанс: Omit зберігає індексні сигнатури. Якщо тип має [key: string]: unknown, вона залишиться в результаті навіть після видалення конкретних ключів.
Типові помилки
1. Omit ключа, якого не існує
interface User {
id: string;
name: string;
}
type Wrong = Omit<User, "nonExistent">;
// Компілюється без помилок. Жодного ефекту.
// TypeScript ігнорує ключі в K, яких немає в T.Жодного краша, жодного попередження. Тип залишається таким самим, а ти витрачаєш час розбираючись, чому нічого не змінилося.
2. Очікування що Omit видаляє вкладені ключі
type UserWithAddress = {
name: string;
address: { street: string; zip: string };
};
// Це не має жодного ефекту:
type Wrong = Omit<UserWithAddress, "address.zip">;
// Результат: { name: string; address: { street: string; zip: string } }Omit поверхневий. Він видаляє тільки ключі верхнього рівня. Щоб прибрати вкладене поле, потрібно вручну перебудувати вкладений тип або написати рекурсивну утиліту.
3. Використання Omit з union-типами без дистрибуції
type Union = string | { id: string; secret: string };
type Attempt = Omit<Union, "secret">;
// Результат не буде таким, як очікується.
// Omit автоматично не розподіляється по членам union.Для union-типів застосовуй Omit до кожного члена окремо.
4. Повернення видаленого ключа через extends
type PublicUser = Omit<User, "password">;
// Це компілюється:
interface AdminUser extends PublicUser {
password: string; // password повернувся
}Структурна типізація це дозволяє. Якщо мета була не пустити password у тип взагалі, цей підхід не захистить. Використовуй перетин типів (intersection) або окрему базу.
5. Плутанина між Omit та Exclude
Exclude<keyof T, K> повертає union з іменами ключів, що залишилися. Omit<T, K> повертає новий тип об'єкта з видаленими ключами. Різні результати, різні сценарії.
Де зустрічається
- React:
Omit<ComponentProps<'input'>, 'ref'>в обгорткахforwardRef(патерн з типів React 18) - Zod:
Omit<z.infer<typeof userSchema>, 'password'>для публічних схем - Prisma:
Omit<Prisma.UserGetPayload<{}>, 'passwordHash'>у типах API-відповідей - tRPC: вхідні дані процедур через
Omit<FullInput, 'internalToken'> - Express: звуження типів тіла запиту в middleware
На практиці Omit найчастіше зустрічається на межах API, де потрібно прибрати чутливі або серверні поля перед відправкою даних клієнту. Цей патерн покриває приблизно 70% випадків реального використання у кодових базах.
Питання для поглиблення
Q: Що відбувається, якщо K містить ключ, якого немає в T?
A: TypeScript компілюється без помилок і ігнорує невідомий ключ. Результуючий тип ідентичний T.
Q: Як Omit поводиться з індексними сигнатурами типу [key: string]: unknown?
A: Зберігає їх. Omit<{ [k: string]: any; foo: string }, 'foo'> залишає індексну сигнатуру і видаляє тільки foo.
Q: Яка різниця між Omit<T, K> і Exclude<keyof T, K>?
A: Exclude повертає union імен ключів. Omit повертає повний тип об'єкта з видаленими ключами. Перше, це рядковий union, друге, це готовий тип для використання.
Q: Як реалізувати Omit самостійно?
A: type MyOmit<T, K extends keyof any> = { [P in keyof T as P extends K ? never : P]: T[P] }. Клауза as у mapped type фільтрує ключі, що збігаються з K, перемапуючи їх у never.
Q: Як Omit поводиться з intersection types на кшталт A & B?
A: Дистрибутивно: Omit<A & B, K> приблизно дорівнює Omit<A, K> & Omit<B, K>. Точний результат залежить від того, у якому саме типі перетину міститься цей ключ.
Приклади
Базовий: видалення чутливого поля
interface User {
id: string;
name: string;
email: string;
password: string;
}
type PublicUser = Omit<User, "password">;
// { id: string; name: string; email: string }
function getPublicUser(user: User): PublicUser {
const { password, ...rest } = user;
return rest; // TypeScript знає, що це відповідає PublicUser
}Паттерн деструктуризації зі spread природно поєднується з Omit: тип описує очікувану структуру, рантайм прибирає реальне значення.
Середній рівень: DTO-типи для CRUD API
interface Product {
id: string;
name: string;
price: number;
description: string;
createdAt: Date;
updatedAt: Date;
}
// POST /products - без id і часових міток (їх встановлює сервер)
type CreateProductDTO = Omit<Product, "id" | "createdAt" | "updatedAt">;
// PATCH /products/:id - id береться з URL, всі поля необов'язкові
type UpdateProductDTO = Partial<Omit<Product, "id">>;
// Ці типи запобігають випадковій передачі серверних полівOmit у поєднанні з Partial покриває більшість CRUD-операцій. Визначаєш вихідний тип один раз і деривуєш решту з нього.
Просунутий рівень: props React-компонента з forwardRef
import { forwardRef, InputHTMLAttributes } from "react";
interface CustomInputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, "ref"> {
label: string;
error?: string;
}
const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>(
({ label, error, ...props }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span>{error}</span>}
</div>
)
);Omit<InputHTMLAttributes<HTMLInputElement>, 'ref'> прибирає вбудований ref, щоб forwardRef міг керувати ним напряму. Саме такий патерн використовується у типах React 18.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.