Angular IDPStrong Junior
🔥 0

Модуль 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: 'Готово',
};
Головна перевага Record

Якщо завтра в TaskStatus зʼявиться 'review', збірка впаде саме тут: «у обʼєкті бракує ключа review». Тобто компілятор сам нагадає дописати переклад — і на екрані ніколи не зʼявиться порожній бейдж. Це найдешевша страховка з усіх, що тут є.

Решта, яку варто знати

ТипЩо робитьТипове застосування
Required<T>Робить усі поля обовʼязковимиПісля заповнення значень за замовчуванням
Exclude<T, U>Прибирає варіанти з unionExclude<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 зроби поле необовʼязковим» — стає видно, що дженерики працюють із типами так само, як звичайні функції з даними.

≈ 45 хв · +25 XP за урок