Модуль 2 · урок 12 з 15
Utility types
Utility types — це готові дженерики, які роблять новий тип зі старого. Лід назвав
чотири: Partial, Pick, Omit, Readonly.
Сенс у них один: описати модель один раз, а всі її варіації вивести автоматично,
щоб вони не розʼїхались.
Базова модель
export interface Task {
id: string;
code: string;
title: string;
status: TaskStatus;
assigneeId: string | null;
estimateHours: number;
createdAt: string;
}
Далі всі приклади — від неї. Головна ідея: жодного разу не переписувати ці поля вручну.
Partial — усі поля необов'язкові
type TaskPatch = Partial<Task>;
// { id?: string; code?: string; title?: string; … }
updateTask(id: string, patch: Partial<Task>) { … }
updateTask('a1', { status: 'done' }); // ок
updateTask('a1', { status: 'done', title: 'Нова' }); // теж ок
updateTask('a1', { titel: 'Нова' }); // ❌ помилка: такого поля немає
Це рівно те, що потрібно для PATCH-запиту: надсилаємо лише змінені поля,
але не можемо надіслати неіснуюче.
Pick — узяти кілька полів
type TaskRow = Pick<Task, 'id' | 'code' | 'title' | 'status'>;
// саме те, що показує таблиця — без зайвого
Omit — усе, крім
type NewTask = Omit<Task, 'id' | 'createdAt'>;
// при створенні id і дати ще немає — їх поставить бекенд
createTask(payload: NewTask): Observable<Task> { … }
Pick і Omit — дзеркальні. Правило вибору просте: якщо потрібні
дві-три поля з двадцяти — Pick; якщо треба прибрати два з двадцяти — Omit.
Другий варіант ще й переживає додавання нових полів у модель.
Readonly — заборонити зміну
type Frozen = Readonly<Task>;
const t: Frozen = getTask();
t.title = 'Інша'; // ❌ помилка компіляції
// для масивів
function render(rows: readonly Task[]) {
rows.push(newTask); // ❌ так функція не зможе зіпсувати вхідні дані
}
readonly у сигнатурі — це обіцянка виклику: «я не зміню те, що ти мені дав».
Особливо доречно в presentation-компонентах із модуля 3: вони показують дані й не мають права
їх мутувати — і тепер це не домовленість, а перевірка компілятора.
Record — словник із типізованим ключем
type TaskStatus = 'todo' | 'in_progress' | 'blocked' | 'done';
const STATUS_LABEL: Record<TaskStatus, string> = {
todo: 'До роботи',
in_progress: 'В роботі',
blocked: 'Заблоковано',
done: 'Готово',
};
Якщо завтра в TaskStatus зʼявиться 'review', збірка впаде саме тут:
«у обʼєкті бракує ключа review». Тобто компілятор сам нагадає дописати переклад — і на екрані
ніколи не зʼявиться порожній бейдж. Це найдешевша страховка з усіх, що тут є.
Решта, яку варто знати
| Тип | Що робить | Типове застосування |
|---|---|---|
Required<T> | Робить усі поля обовʼязковими | Після заповнення значень за замовчуванням |
Exclude<T, U> | Прибирає варіанти з union | Exclude<TaskStatus, 'done'> — активні статуси |
Extract<T, U> | Лишає тільки збіги | Звузити union до потрібних |
NonNullable<T> | Прибирає null і undefined | Після перевірки на порожнечу |
ReturnType<typeof fn> | Тип того, що повертає функція | Не дублювати тип результату |
Parameters<typeof fn> | Кортеж типів аргументів | Обгортки над функціями |
Awaited<T> | Розгортає Promise | Тип результату async-функції |
Як вони комбінуються
// чернетка форми: усе, крім службових полів, і все необов'язкове
type TaskDraft = Partial<Omit<Task, 'id' | 'createdAt'>>;
// незмінний рядок таблиці лише з потрібними полями
type Row = Readonly<Pick<Task, 'id' | 'code' | 'status'>>;
// тип результату сервісу без його дублювання
type TaskList = Awaited<ReturnType<TaskApiService['getTasks']>>;
Одна модель — одне джерело правди. Якщо в Task зʼявиться нове поле,
воно саме потрапить у TaskPatch, у чернетку форми й у DTO створення.
Якщо ж усі ці типи написані руками, вони поступово розʼїдуться — і рано чи пізно щось
відвалиться саме там, де ніхто не дивився.
Як вони влаштовані всередині
// Partial — це буквально три рядки
type MyPartial<T> = { [K in keyof T]?: T[K] };
// Readonly — так само
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
// Pick — вибрати підмножину ключів
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
// Omit — узяти все, крім переліченого
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;
Ніякої магії: це звичайні відображені типи. Якщо прочитати їх уголос — «для кожного ключа з T зроби поле необовʼязковим» — стає видно, що дженерики працюють із типами так само, як звичайні функції з даними.