Коротка відповідь. 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 | undefined

TValue | 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 – питання про решту стають передбачуваними.

Типові помилки

  1. Протікання any. Один as any усередині generic-функції зводить нанівець усю типізацію. Якщо без приведення не обійтись – використовуй unknown і звужуй перевірками.
  2. Generic заради generic. Якщо параметр типу використовується лише один раз у сигнатурі (function log<T>(value: T): void), він нічого не дає – достатньо unknown. Правило: generic виправданий, коли тип з'являється щонайменше двічі (вхід і вихід, або два аргументи).
  3. Занадто розумні типи. П'ятиповерховий 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 уже окупились.