Модуль 2 · урок 13 з 15
Моделювання даних
Типи — це не бюрократія навколо коду, а спосіб проєктування. Добре описана модель робить неправильні стани невимовними: помилку не треба ловити тестом, її просто неможливо написати. Цей урок про те, як цим користуватись.
DTO і доменна модель — це різні речі
// те, що реально приходить із бекенду
export interface TaskDto {
id: string;
due_date: string | null; // snake_case, дата рядком
assignee_id: string | null;
estimate_hours: number;
}
// те, чим зручно користуватись у застосунку
export interface Task {
id: string;
dueDate: Date | null;
assigneeId: string | null;
estimateHours: number;
}
// місток між ними — в одному місці
export function toTask(dto: TaskDto): Task {
return {
id: dto.id,
dueDate: dto.due_date ? new Date(dto.due_date) : null,
assigneeId: dto.assignee_id,
estimateHours: dto.estimate_hours,
};
}
Коли бекенд перейменує поле або поміняє формат дати, ти правиш одну функцію, а не
сорок компонентів. Це та сама ізоляція, про яку йдеться в модулі 4: мок-сервіс віддає такий
самий Task, тож потім реальний API підключається без переписування UI.
Літеральні типи замість магічних рядків
// ❌ так помилку видно тільки в проді
interface Task { status: string; }
task.status = 'complete'; // хтось напише так, хтось 'completed', хтось 'done'
// ✅ так вона не компілюється
export type TaskStatus = 'todo' | 'in_progress' | 'blocked' | 'done';
interface Task { status: TaskStatus; }
task.status = 'complete'; // ❌ помилка одразу
// список для випадайки — виводиться з типу, а не дублюється
export const TASK_STATUSES = ['todo', 'in_progress', 'blocked', 'done'] as const;
export type TaskStatus = typeof TASK_STATUSES[number];
as const каже TypeScript: «це не просто масив рядків, це саме ці чотири значення».
Далі тип виводиться з масиву — і додати новий статус можна в одному місці.
Розрізнюваний union: неможливий стан стає невимовним
Класична проблема: компонент має три поля — loading, data,
error. Скільки комбінацій вони дають? Вісім. Скільки з них осмислені? Три.
// ❌ можна описати стан, якого не буває
interface State {
loading: boolean;
data: Task[] | null;
error: string | null;
}
const bad: State = { loading: true, data: [...], error: 'ой' }; // і що це означає?
// ✅ рівно три можливі стани
type State =
| { status: 'loading' }
| { status: 'ready'; data: Task[] }
| { status: 'error'; message: string };
function render(state: State) {
switch (state.status) {
case 'loading': return spinner();
case 'ready': return table(state.data); // TS знає, що data тут є
case 'error': return message(state.message); // а тут є message
}
}
Звуження типів і type guards
// вбудовані способи звуження
if (typeof value === 'string') { … }
if (Array.isArray(value)) { … }
if (value instanceof Date) { … }
if ('data' in state) { … }
if (task.assigneeId !== null) { … }
// власний type guard: функція, яка ДОВОДИТЬ тип
export function isTask(value: unknown): value is Task {
return typeof value === 'object'
&& value !== null
&& 'id' in value
&& typeof (value as { id: unknown }).id === 'string';
}
const raw: unknown = JSON.parse(text);
if (isTask(raw)) {
raw.id; // TypeScript тепер знає, що це Task
}
Запис value is Task — це обіцянка компілятору: «якщо функція повернула
true, значення точно такого типу». Саме так виглядає чесна альтернатива
as any для даних, у яких ти не впевнений.
Перевірка повноти switch
function label(status: TaskStatus): string {
switch (status) {
case 'todo': return 'До роботи';
case 'in_progress': return 'В роботі';
case 'blocked': return 'Заблоковано';
case 'done': return 'Готово';
default: {
const never: never = status; // ❗ якщо додати новий статус — тут буде помилка
return never;
}
}
}
Тип never означає «сюди неможливо потрапити». Якщо в union додасться пʼятий
варіант, компілятор скаже, що його не можна присвоїти never — і покаже саме це місце.
Прийом дешевий, а рятує від забутих гілок у десятках місць.
Роби неправильні стани невимовними. Замість того, щоб перевіряти в рантаймі, що «якщо loading, то data має бути null» — опиши тип так, щоб інакше й не вийшло. Це головна ідея всього уроку, і саме її варто сказати ліду, якщо мова зайде про типізацію.