Master TypeScript's Built-In Utility Types for Cleaner Code
Learn how TypeScript's utility types like Partial, Pick, Omit, and Record eliminate duplicate interfaces and keep type definitions in sync automatically.
Припиніть копіювати та вставляти інтерфейси: пишіть більш чистий та надійний код за допомогою вбудованих трансформаторів типів TypeScript.
Рано чи пізно кожен розробник на TypeScript стикається з однією й тією самою проблемою. Ви створюєте чистий інтерфейс User з вісьмома полями. Через кілька хвилин вам потрібна форма для редагування, де насправді потрібні лише два поля, тож ви створюєте інтерфейс UpdateUserInput. Потім з’являється відповідь API, яка має приховувати конфіденційні поля, тож ви додаєте ще один інтерфейс — PublicUser. Незабаром значна частина вашого кодового базису складається з майже ідентичних інтерфейсів, які поступово втрачають синхронність у міру розвитку проекту.
Щоразу, коли змінюється основна схема, вам доводиться шукати та оновлювати кілька місць одночасно. Якщо ви щось забудете, це призведе до появи багу, який проявиться пізніше у продакшені.
TypeScript вирішує цю проблему за допомогою вбудованого набору інструментів під назвою Utility Types. Їх можна уявити як функції, які працюють із типами, а не з значеннями: ви передаєте на вхід існуючий тип, застосовуєте певне правило трансформації та отримуєте абсолютно новий тип. Це дозволяє зберігати одне авторитетне визначення та делегувати всю рутинну роботу з перетворення типів компілятору.
Нижче наведені типи-інструменти, які варто використовувати постійно, описані простою мовою з реальними фрагментами коду, які можна використовувати одразу.
1. Трансформація суворості властивостей
Ці три інструменти контролюють, чи є поля необов’язковими, обов’язковими чи замороженими.
Partial<Type>
Простою мовою: „Зробити кожну властивість необов’язковою.“
Коли ви створюєте щось на кшталт кінцевої точки PATCH або оновлюєте частину стану додатку, ви не хочете вимагати від користувача всю структуру об’єкта. Partial<T> переглядає кожне поле інтерфейсу та додає до кожного з них символ ?.
interface UserProfile {
id: string;
name: string;
email: string;
avatarUrl: string;
theme: "light" | "dark";
}
// Without Partial, you'd have to rewrite all fields with '?'
type UpdateProfileInput = Partial<UserProfile>;
function updateProfile(userId: string, changes: UpdateProfileInput) {
// Valid: changes can include any subset of UserProfile properties
apiClient.patch(`/users/${userId}`, changes);
}
// Valid call:
updateProfile("usr_101", { theme: "dark" });
Required<Type>
Простими словами: "Видаліть усі опційні позначки, щоб нічого не можна було пропустити."
Required<T> робить протилежне Partial<T>: якщо деякі поля інтерфейсу є опційними, він змушує їх усі бути присутніми. Це корисно, коли ви об’єднуєте стандартні налаштування та потребуєте гарантувати, що нічого не буде відсутньо.
interface AppConfig {
apiUrl: string;
timeout?: number;
retries?: number;
enableLogging?: boolean;
}
// Ensure the internal runner has every single setting resolved
type ResolvedConfig = Required<AppConfig>;
const defaultConfig: ResolvedConfig = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
enableLogging: true,
};
function initializeApp(userOptions: AppConfig): ResolvedConfig {
return { ...defaultConfig, ...userOptions };
}
Readonly<Type>
Простою мовою: "Заблокуйте кожну властивість, щоб її не можна було переатрибувати."
Коли ви хочете запобігти випадковим змінам — мова йде про значення конфігурації, параметри компонентів чи спільний стан додатку — флаг Readonly<T> встановлює кожну властивість у режим readonly. Будь-яка спроба змінити ці значення пізніше призводить до помилки на етапі компіляції.
interface SystemRole {
roleName: string;
permissions: string[];
}
const SuperAdminRole: Readonly<SystemRole> = {
roleName: "SUPER_ADMIN",
permissions: ["read", "write", "delete", "admin"],
};
// Error: Cannot assign to 'roleName' because it is a read-only property.
SuperAdminRole.roleName = "USER";
2. Вибір необхідних елементів
Існує багато ситуацій, коли вам потрібна лише частина інтерфейсу. Замість того, щоб створювати зовсім новий, непов’язаний тип, ви можете безпосередньо взяти потрібні елементи з ваших існуючих моделей домену.
Pick<Type, Keys>
Простою мовою: "Створіть новий тип, використовуючи лише ці вказані ключі з оригіналу."
Припустимо, у вас є велика модель, але невеликий UI-віджет чи легка запит до бази даних потребує лише імені та аватара. Ви можете наказати TypeScript зберігати лише ці два поля.
interface Product {
id: string;
sku: string;
name: string;
price: number;
stockQuantity: number;
supplierId: string;
description: string;
}
// Pick only the fields a mini checkout card displays
type CartItemPreview = Pick<Product, "id" | "name" | "price">;
const item: CartItemPreview = {
id: "prod_99",
name: "Wireless Mechanical Keyboard",
price: 129.99,
};
Omit<Type, Keys>
Простою мовою: "Залиште весь початковий тип, але видаліть ці конкретні поля."
Omit<T, K> змінює функцію Pick на протилежну. Коли ви додаєте новий запис у базу даних, зазвичай у вас є всі поля, окрім тих, які система генерує автоматично, наприклад id чи createdAt.
interface Article {
id: number;
title: string;
slug: string;
content: string;
publishedAt: Date;
viewCount: number;
}
// Strip metadata handled by the database
type CreateArticlePayload = Omit<Article, "id" | "viewCount" | "publishedAt">;
function submitArticle(payload: CreateArticlePayload) {
// TypeScript guarantees no one accidentally submits an 'id' or 'viewCount'
apiClient.post("/articles", payload);
}
3. Динамічне мапування ключів
Record<Keys, Type>
Простою мовою: «Створіть словник, ключі якого беруться з цього списку, а значення мають відповідати певній структурі».
Об’єкти JavaScript, які використовуються як таблиці пошуку, часто мають нечітко визначений тип, наприклад { [key: string]: any }, що позбавляє систему безпеки типів її функціональності. Record<Keys, Type> замість цього чітко визначає, які ключі дозволені та якою має бути структура їхніх значень.
type SubscriptionTier = "free" | "pro" | "enterprise";
interface TierFeatures {
monthlyQuota: number;
customDomainAllowed: boolean;
supportLevel: "community" | "email" | "dedicated";
}
// Guarantees all three tiers are explicitly handled
const subscriptionPlans: Record<SubscriptionTier, TierFeatures> = {
free: {
monthlyQuota: 1000,
customDomainAllowed: false,
supportLevel: "community",
},
pro: {
monthlyQuota: 50000,
customDomainAllowed: true,
supportLevel: "email",
},
enterprise: {
monthlyQuota: 1000000,
customDomainAllowed: true,
supportLevel: "dedicated",
},
};
Якщо з часом до SubscriptionTier буде додано новий рівень „starter“, TypeScript одразу позначить subscriptionPlans та повідомить про відсутність запису „starter“.
4. Видалення та обмеження типів уніону
Вищезгадані функції змінюють форму ключів об’єктів. Наступна пара функцій працює з типами об’єднання, такими як "apple" | "banana" | "orange".
Exclude<UnionType, ExcludedMembers>
Простими словами: "Візьміть цей набір можливих значень та видаліть ті, що перелічені тут."
type TaskStatus = "draft" | "in_progress" | "review" | "completed" | "archived";
// You can edit tasks in any status except when they are completed or archived
type EditableTaskStatus = Exclude<TaskStatus, "completed" | "archived">;
// Result: "draft" | "in_progress" | "review"
Extract<UnionType, ExtractedMembers>
Простими словами: "Візьміть цей набір можливих значень та залиште лише ті, що відповідають цим критеріям."
Extract працює у зворотному напрямку до Exclude: він ізолює перетин між двома типами об’єднання, залишаючи лише ті елементи, які існують у обох.
type WindowEvents = "click" | "scroll" | "mousemove" | "keydown" | "keyup";
type PointerEvents = "click" | "mouseenter" | "mouseleave" | "mousemove";
// Keep only the events shared across both categories
type SharedEvents = Extract<WindowEvents, PointerEvents>;
// Result: "click" | "mousemove"
NonNullable<Type>
Простими словами: видаляється значення null та undefined з типу.
Під час роботи з даними, які надходять від зовнішніх API чи клієнтів баз даних, поля часто мають тип string | null | undefined. NonNullable<T> усуває ці можливості порожніх значень, залишаючи лише справжні типи.
type SearchQuery = string | string[] | null | undefined;
type CleanSearchQuery = NonNullable<SearchQuery>;
// Result: string | string[]
5. Визначення типів з існуючого коду
Одним із найкращих способів економії часу в TypeScript є отримання типів безпосередньо з функцій, обіцянок та імпортованих модулів замість їхньої ручної передекларації.
ReturnType<FunctionType>
Простими словами: він повертає те, що повертає функція, тож вам ніколи не доведеться вводити цю структуру вручну.
Для складних функцій-фабрик, селекторів у стилі Redux або сторонніх пакетів, які не розкривають свої формати повернення, ви можете отримати точну структуру, посилаючись на саму функцію.
function buildSessionPayload(userId: string, roles: string[]) {
return {
sessionId: crypto.randomUUID(),
authenticatedAt: Date.now(),
expiresAt: Date.now() + 1000 * 60 * 60 * 24,
account: {
id: userId,
primaryRole: roles[0] ?? "guest",
allRoles: roles,
},
};
}
// Automatically inherits the shape of whatever buildSessionPayload returns
type UserSession = ReturnType<typeof buildSessionPayload>;
Якщо об’єкт, повернутий усередині buildSessionPayload, зміниться пізніше, UserSession автоматично врахує цю зміну.
Parameters<FunctionType>
Простими словами: він створює тупл, у якому перераховані всі аргументи, що приймає функція.
Кожного разу, коли ви обгортаєте метод SDK, передаєте аргументи чи створюєте шар логування навколо виклику функції, Parameters<T> гарантує, що сигнатура вашого обгортки ніколи не відхилиться від оригінальної.
function triggerAlert(message: string, severity: "low" | "high", code?: number) {
// Internal dispatch logic
}
// Extracts arguments as a tuple: [message: string, severity: "low" | "high", code?: number]
type AlertArgs = Parameters<typeof triggerAlert>;
function logAndAlert(...args: AlertArgs) {
console.log("Dispatching alert:", args[0]);
triggerAlert(...args);
}
Awaited<Type>
Простими словами: цей метод розпаковує об’єкт Promise та виводить значення, яке знаходиться всередині.
Оскільки багато сучасних рішень у TypeScript базуються на асинхронному коді, Awaited<T> є необхідним для поступового розбирання одного або кількох рівнів вкладених об’єктів Promise, поки не буде отримано фактичне значення.
async function fetchAccountDetails() {
return {
accountId: "acct_8472",
balance: 4250.75,
currency: "USD",
};
}
// Unwraps the Promise<T> returned by the async function
type AccountDetails = Awaited<ReturnType<typeof fetchAccountDetails>>;
// Result: { accountId: string; balance: number; currency: string }
const cachedAccount: AccountDetails = {
accountId: "acct_8472",
balance: 4250.75,
currency: "USD",
};
6. Використання утилітних типів як будівельних блоків
Утилітні типи демонструють свою справжню силу при поєднанні. Оскільки кожен з них генерує стандартний тип TypeScript, результат роботи одного з них можна безпосередньо використовувати як вхідні дані для іншого.
Розгляньмо шар даних, створений на основі елемента Customer:
interface Customer {
id: string;
firstName: string;
lastName: string;
email: string;
phoneNumber?: string;
createdAt: Date;
updatedAt: Date;
}
// 1. Creation payload: No auto-generated fields, but firstName, lastName, and email are mandatory
type CreateCustomerDTO = Omit<Customer, "id" | "createdAt" | "updatedAt">;
// 2. Update payload: Omit IDs, and let the user modify ANY valid field optionally
type UpdateCustomerDTO = Partial<Omit<Customer, "id" | "createdAt" | "updatedAt">>;
// 3. Read-only view for UI caches
type ReadonlyCustomer = Readonly<Customer>;
Лише кількома рядками ви створили три окремі об’єкти передачі даних із повною перевіркою типів. Якщо базовий інтерфейс Customer згодом отримає нову властивість — скажімо, loyaltyTier — усі три похідні типи оновляться автоматично, без необхідності ручних змін.
Короткий огляд
Якщо розглядати типи як вручну скопійовані копії, кожна зміна схеми стає тягарем для підтримки. Утилітні типи дозволяють визначити вашу бізнес-модель один раз, і компілятор генеруватиме всі необхідні варіації з цього єдиного визначення. Це призводить до більш лаконічного коду, безпечніших рефакторингів та меншої кількості часу, витраченого на узгодження майже ідентичних інтерфейсів, щоб ви могли зосередитися на розробці функцій.
Пов’язана література
- Моделювання доменів у TypeScript: за межами базових анотацій типів — Дізнайтеся про практичні прийоми використання TypeScript — від розрізнення значень unknown та any до конструкцій discriminated unions та satisfies — які допомагають моделювати коректні стани замість простого позначення даних.
- Поширені помилки в JavaScript та TypeScript, які тихо ламають код — Пояснюється суттєві нюанси JavaScript та TypeScript — від порівнянь з NaN до проблем із асинхронними операціями та примусовою зміною типу — які спричиняють баги, незважаючи на зовнішню правильність коду.