Модуль 5 · урок 8 з 10
Типізація колонок
«Strongly typed models» — окрема вимога в цілі, і саме в таблиці вона найкорисніша. Мета проста: зробити так, щоб колонка не могла послатись на неіснуюче поле, а тип значення в комірці визначався сам. Тут нарешті знадобляться generics із модуля 2.
Проблема нетипізованих колонок
// ❌ так пишуть найчастіше — і так найлегше зламати
const columns = [
{ key: 'code', label: 'Код' },
{ key: 'titel', label: 'Назва' }, // друкарська помилка, комірка буде порожня
{ key: 'assignee.name', label: 'Виконавець' }, // такого поля немає
];
Помилка не впаде: у комірці просто буде порожньо. І ти шукатимеш причину, дивлячись на дані, а не на конфіг колонок.
Column<T> із keyof
export interface Column<T> {
key: keyof T; // лише реальні поля моделі
label: string;
sortable?: boolean;
width?: string;
align?: 'start' | 'end';
}
const columns: Column<Task>[] = [
{ key: 'code', label: 'Код', sortable: true, width: '140px' },
{ key: 'title', label: 'Назва', sortable: true },
{ key: 'estimateHours', label: 'Годин', align: 'end' },
{ key: 'titel', label: 'Назва' }, // ❌ помилка компіляції — саме те, що треба
];
Перейменували поле в моделі — збірка впаде саме на конфізі колонок, і IDE запропонує виправити. Без типізації ця сама зміна тихо зробить одну колонку порожньою, і помітять це в кращому разі на рев'ю, у гіршому — користувачі.
Форматування зі збереженням типу
export interface Column<T, K extends keyof T = keyof T> {
key: K;
label: string;
format?: (value: T[K], row: T) => string; // тип значення виводиться з ключа
}
const columns: Column<Task>[] = [
{
key: 'dueDate',
label: 'Термін',
format: v => formatDate(v), // v має тип string — тип поля dueDate
},
{
key: 'estimateHours',
label: 'Годин',
format: v => `${v} год`, // а тут v — number
},
];
Це і є той момент, заради якого існують дженерики: тип аргументу у функції форматування
не вказаний руками, він виведений із того, яке поле вказане в key.
Обчислювані колонки
Не все, що показує таблиця, є полем моделі. Виконавець приходить окремим списком, «прострочено» рахується з дати. Для таких випадків потрібен другий вид колонки:
export type Column<T> =
| { kind: 'field'; key: keyof T; label: string; sortable?: boolean }
| { kind: 'computed'; id: string; label: string; value: (row: T) => string };
const columns: Column<Task>[] = [
{ kind: 'field', key: 'code', label: 'Код', sortable: true },
{ kind: 'computed', id: 'assignee', label: 'Виконавець',
value: row => this.userName(row.assigneeId) },
];
@switch (column.kind) {
@case ('field') { {{ row[column.key] }} }
@case ('computed') { {{ column.value(row) }} }
}
Це розрізнюваний union із модуля 2 у чистому вигляді: у кожній гілці доступні саме ті поля, які там існують, і четвертий варіант описати неможливо.
Типізований доступ до значення
function getValue<T, K extends keyof T>(row: T, key: K): T[K] {
return row[key];
}
const code = getValue(task, 'code'); // string
const hours = getValue(task, 'estimateHours'); // number
const oops = getValue(task, 'nope'); // ❌ помилка компіляції
Типізація стану сортування
export interface SortState<T> {
key: keyof T;
dir: 'asc' | 'desc';
}
// тепер і сортувати за неіснуючим полем не вийде
setSort(key: keyof Task, dir: 'asc' | 'desc'): void { … }
Типізацію можна довести до абсурду — описати ще й формат кожної комірки, ширини й вирівнювання типами. Практичний орієнтир такий: типізуй те, що ламається мовчки. Неправильний ключ поля ламається мовчки — його типізуємо. Неправильна ширина колонки видно одразу оком — її досить рядком.