Skip to main content

Утиліта type omit в TypeScript

Omit<T, K> - утилітний тип, який бере тип T і повертає його копію без ключів, перелічених у K.

Теорія

TL;DR

  • Уяви форму, де певні поля замальовані маркером: решта залишається, тільки вказані поля зникають
  • Головна різниця від Pick: Omit віднімає ключі від повного типу; Pick будує новий тип, вибираючи тільки те, що ти назвав
  • Правило вибору: виключень менше, ніж залишків? Бери Omit. Потрібна мала підмножина? Бери Pick
  • Жодного впливу на рантайм: стирається під час компіляції у JavaScript
  • Працює тільки на верхньому рівні ключів; шляхи на кшталт "address.zip" не мають жодного ефекту

Швидкий приклад

typescript
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

Ці два типи можуть давати однаковий результат:

typescript
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
// Вбудоване визначення 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 ключа, якого не існує

typescript
interface User { id: string; name: string; } type Wrong = Omit<User, "nonExistent">; // Компілюється без помилок. Жодного ефекту. // TypeScript ігнорує ключі в K, яких немає в T.

Жодного краша, жодного попередження. Тип залишається таким самим, а ти витрачаєш час розбираючись, чому нічого не змінилося.

2. Очікування що Omit видаляє вкладені ключі

typescript
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-типами без дистрибуції

typescript
type Union = string | { id: string; secret: string }; type Attempt = Omit<Union, "secret">; // Результат не буде таким, як очікується. // Omit автоматично не розподіляється по членам union.

Для union-типів застосовуй Omit до кожного члена окремо.

4. Повернення видаленого ключа через extends

typescript
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>. Точний результат залежить від того, у якому саме типі перетину міститься цей ключ.

Приклади

Базовий: видалення чутливого поля

typescript
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

typescript
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

typescript
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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?