Коротка відповідь. Generics – це спосіб написати функцію чи структуру один раз так, щоб вона працювала з різними типами й не губила інформацію про тип. Якщо ти повертаєш any – TypeScript вимикається; якщо дублюєш код під кожен тип – програєш у підтримці. Generics вирішують обидві проблеми. Нижче – мінімум теорії і чотири практичні приклади, які реально просять пояснити на співбесідах.
Проблема, яку вирішують generics
Уяви функцію, що обгортає значення в масив:
function wrapInArray(value: any): any[] {
return [value];
}
const numbers = wrapInArray(42);
numbers[0].toUpperCase(); // компілюється, падає в рантайміany тут «з'їв» тип: TypeScript більше нічого не перевіряє. Generic-версія зберігає зв'язок між входом і виходом:
function wrapInArray<T>(value: T): T[] {
return [value];
}
const numbers = wrapInArray(42); // number[]
numbers[0].toUpperCase(); // помилка компіляції – і це добреT – параметр типу: він підставляється в момент виклику, найчастіше автоматично через inference. Це головна думка для співбесіди: generic не «будь-який тип», а «конкретний тип, який ми дізнаємось пізніше».
Constraints: extends і keyof
Без обмежень T може бути чим завгодно, тому всередині функції з ним майже нічого не можна робити. extends звужує допустимі типи:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest('обід', 'вечеря'); // ок: у string є length
longest([1, 2], [3]); // ок: у масива є length
longest(10, 20); // помилка: у number немає lengthКласика співбесід – типобезпечний pick, де keyof зв'язує ключ з об'єктом:
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) {
result[key] = obj[key];
}
return result;
}
const user = { id: 1, name: 'Оля', email: 'o@example.com' };
const preview = pick(user, ['id', 'name']); // { id: number; name: string }
pick(user, ['id', 'phone']); // помилка: 'phone' не існує в userТут варто вміти пояснити кожен елемент: K extends keyof T означає «K – це підмножина ключів T», тому неможливо запросити неіснуюче поле, а тип результату звужується автоматично.
Дефолтні параметри типу
Параметр типу може мати значення за замовчуванням – зручно для обгорток API:
type ApiResponse<TData = unknown> = {
ok: boolean;
status: number;
data: TData;
};
async function fetchJson<TData = unknown>(url: string): Promise<ApiResponse<TData>> {
const response = await fetch(url);
return {
ok: response.ok,
status: response.status,
data: (await response.json()) as TData,
};
}
type User = { id: number; name: string };
const res = await fetchJson<User>('/api/user/1');
res.data.name; // string – тип доїхав до місця використанняЗверни увагу на дефолт unknown, а не any: якщо тип не вказали, TypeScript змусить перевірити дані перед використанням. Це чесніше відображає реальність – з мережі може прийти будь-що.
Приклад: типізований кеш
Невеликий клас, який показує generics на рівні типу, методу і constraint одночасно:
class SimpleCache<TValue> {
private store = new Map<string, { value: TValue; expiresAt: number }>();
set(key: string, value: TValue, ttlMs: number): void {
this.store.set(key, { value, expiresAt: Date.now() + ttlMs });
}
get(key: string): TValue | undefined {
const entry = this.store.get(key);
if (!entry) return undefined;
if (Date.now() > entry.expiresAt) {
this.store.delete(key);
return undefined;
}
return entry.value;
}
}
const userCache = new SimpleCache<User>();
userCache.set('user:1', { id: 1, name: 'Оля' }, 60_000);
const cached = userCache.get('user:1'); // User | undefinedTValue | undefined у поверненні – теж тема для розмови: тип чесно каже, що значення може не бути, і компілятор змусить це обробити.
Generics у React-компонентах і хуках
Фронтендерам generics найчастіше трапляються не в утилітах, а в компонентах. Типовий приклад – список, який працює з будь-яким типом елементів:
type ListProps<TItem> = {
items: TItem[];
getKey: (item: TItem) => string;
renderItem: (item: TItem) => React.ReactNode;
};
function List<TItem>({ items, getKey, renderItem }: ListProps<TItem>) {
return (
<ul>
{items.map((item) => (
<li key={getKey(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Тип item виводиться сам: у renderItem це User, без жодного as
<List
items={users}
getKey={(user) => String(user.id)}
renderItem={(user) => <span>{user.name}</span>}
/>;Сила прикладу в тому, що TItem виводиться з items, і всі три пропси стають узгодженими: передати getKey, який чекає інший тип, неможливо. Той самий патерн – у типізованих хуках на кшталт useLocalStorage<T>(key: string, initial: T).
Як влаштовані вбудовані utility types
Питання «поясни, як працює Partial» – спосіб перевірити розуміння generics зсередини. Більшість утиліт TypeScript – це короткі generic-типи:
// Спрощені визначення з lib.es5.d.ts
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyRecord<K extends keyof any, V> = { [P in K]: V };
type MyReturnType<T> = T extends (...args: never[]) => infer R ? R : never;Розшифрувати варто вміти хоча б Partial: «пройдись по всіх ключах T і зроби кожен опціональним». Якщо можеш своїми словами пояснити один mapped type – питання про решту стають передбачуваними.
Типові помилки
- Протікання
any. Одинas anyусередині generic-функції зводить нанівець усю типізацію. Якщо без приведення не обійтись – використовуйunknownі звужуй перевірками. - Generic заради generic. Якщо параметр типу використовується лише один раз у сигнатурі (
function log<T>(value: T): void), він нічого не дає – достатньоunknown. Правило: generic виправданий, коли тип з'являється щонайменше двічі (вхід і вихід, або два аргументи). - Занадто розумні типи. П'ятиповерховий conditional type, який ніхто в команді не може прочитати, гірший за простіший тип із невеликим дублюванням. На співбесіді чесна відповідь «тут я обрав би простіший тип заради читабельності» звучить сильно, а не слабко.
Питання зі співбесід
Чим generics відрізняються від any? any вимикає перевірку типів; generic зберігає її, відкладаючи конкретний тип до моменту використання. З any помилка виявиться в рантаймі, з generic – під час компіляції.
Що означає <K extends keyof T>? K обмежений об'єднанням ключів типу T. Це дозволяє безпечно індексувати T[K] і не дає передати неіснуючий ключ.
Коли дефолтний параметр типу корисний? Коли є розумний фолбек: ApiResponse<TData = unknown> дає змогу не вказувати тип там, де він не потрібен, не втрачаючи строгість.
Чому unknown кращий за any для даних ззовні? unknown вимагає звуження (перевірки типу) перед використанням, any дозволяє все й одразу. Детальніше – в офіційній документації TypeScript.
Куди далі
Generics найчастіше перевіряють у зв'язці з практикою: на live-coding задачах із JavaScript і React-задачах із TypeScript типізація – половина оцінки. Загальну картину питань співбесіди дивись у гайді з JavaScript-співбесіди.
Практичне завдання: напиши функцію groupBy<T, K extends keyof T>(items: T[], key: K): Map<T[K], T[]> без підглядання, а потім перевір типи на масиві об'єктів із різними полями. Якщо компілятор зловив хоч одну твою помилку – generics уже окупились.