Angular IDPStrong Junior
Модулі / Грид / Типізація колонок
🔥 0

Модуль 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 { … }
Де зупинитись

Типізацію можна довести до абсурду — описати ще й формат кожної комірки, ширини й вирівнювання типами. Практичний орієнтир такий: типізуй те, що ламається мовчки. Неправильний ключ поля ламається мовчки — його типізуємо. Неправильна ширина колонки видно одразу оком — її досить рядком.

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