Angular IDPStrong Junior
🔥 0

Модуль 2 · урок 11 з 15

Generics

Дженерик — це тип, який стає відомим у момент використання. Звучить абстрактно, поки не побачиш проблему, яку він вирішує: без них доводиться або дублювати код на кожен тип, або писати any і втрачати всю користь від TypeScript.

Проблема, з якої все починається

// варіант 1: дублювати
function firstTask(items: Task[]): Task | undefined { return items[0]; }
function firstUser(items: User[]): User | undefined { return items[0]; }

// варіант 2: здатись
function first(items: any[]): any { return items[0]; }
// тепер firstTask.title не підказується, а помилки не ловляться

// варіант 3: дженерик
function first<T>(items: T[]): T | undefined { return items[0]; }

const t = first(tasks);   // TypeScript сам вивів: Task | undefined
const u = first(users);   // User | undefined

<T> — це «змінна для типу». Ти не називаєш конкретний тип, а кажеш: який тип прийде на вхід — такий і буде на виході. І в більшості випадків його навіть не треба вказувати вручну: TypeScript виводить його сам.

Обмеження: extends

// ❌ T може бути чим завгодно — поля id може не бути
function byId<T>(items: T[], id: string): T | undefined {
  return items.find(i => i.id === id);   // помилка: у T немає id
}

// ✅ звужуємо: T — будь-що, у чого є id
function byId<T extends { id: string }>(items: T[], id: string): T | undefined {
  return items.find(i => i.id === id);
}

byId(tasks, 'a1');    // ок — у Task є id
byId([1, 2, 3], 'x'); // ❌ помилка ще під час написання

extends тут не має нічого спільного з наслідуванням класів. Це просто умова: «будь-який тип, який задовольняє цю форму».

keyof: обмеження полем об'єкта

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map(item => item[key]);
}

const titles = pluck(tasks, 'title');   // string[]
const hours = pluck(tasks, 'estimate'); // number[]
pluck(tasks, 'titel');                  // ❌ помилка: такого поля немає

Це той самий прийом, який знадобиться в модулі 5 для типізації колонок таблиці: колонка не зможе послатись на неіснуюче поле, а тип значення визначиться сам.

// модель колонки з модуля 5, наперед
interface Column<T> {
  key: keyof T;
  label: string;
  sortable?: boolean;
}

const columns: Column<Task>[] = [
  { key: 'code', label: 'Код', sortable: true },
  { key: 'title', label: 'Назва' },
  { key: 'assigne', label: 'Виконавець' },   // ❌ друкарська помилка спіймана
];

Дженерики в сервісах

@Injectable({ providedIn: 'root' })
export class ApiService {
  private http = inject(HttpClient);

  get<T>(url: string): Observable<T> {
    return this.http.get<T>(url);
  }
}

// на місці виклику тип відомий
const tasks$ = api.get<Task[]>('/api/tasks');   // Observable<Task[]>

Так само влаштований і сам HttpClient: http.get<Task[]>(...). Це найчастіший дженерик, який ти писатимеш у модулі 4.

Кілька параметрів і значення за замовчуванням

interface ApiResult<TData, TError = string> {
  data: TData | null;
  error: TError | null;
  loading: boolean;
}

const a: ApiResult<Task[]> = { data: null, error: null, loading: true };
const b: ApiResult<User, HttpErrorResponse> = { … };

Дженерик-класи

export class Store<T> {
  private value: T;

  constructor(initial: T) { this.value = initial; }

  get(): T { return this.value; }
  set(next: T): void { this.value = next; }
  update(fn: (prev: T) => T): void { this.value = fn(this.value); }
}

const filter = new Store({ status: 'all', query: '' });
filter.update(f => ({ ...f, query: 'kyiv' }));   // тип поля підказується
Іменування

Одна літера — традиція, а не правило: T — тип, K — ключ, V — значення, E — помилка. Коли параметрів більше двох або зміст неочевидний, краще писати повні імена: <TData, TError>. Читабельність важливіша за традицію.

Коли дженерик не потрібен

Дженерик доречний
  • Функція працює однаково з будь-яким типом: first, byId.
  • Тип результату залежить від типу аргументу.
  • Контейнер: стор, кеш, результат запиту.
Зайва складність
  • Параметр типу використовується рівно один раз — тоді достатньо unknown.
  • Функція насправді працює лише з одним типом.
  • Три-чотири параметри типу заради «гнучкості на майбутнє».

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