Модуль 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. - Функція насправді працює лише з одним типом.
- Три-чотири параметри типу заради «гнучкості на майбутнє».