# TypeScript Generics: приклади, які варто вміти пояснити

> Generics у TypeScript без магії: навіщо вони потрібні, як працюють constraints, keyof і дефолтні параметри типу. Практичні приклади, типові помилки та питання зі співбесід.

- Автор: Юра Скиба (https://cookiesoftware.io)
- Опубліковано: 2026-09-24
- Категорія: JavaScript і TypeScript
- Canonical: https://cookiesoftware.io/blog/typescript-generics

---

**Коротка відповідь.** Generics – це спосіб написати функцію чи структуру один раз так, щоб вона працювала з різними типами й **не губила інформацію про тип**. Якщо ти повертаєш `any` – TypeScript вимикається; якщо дублюєш код під кожен тип – програєш у підтримці. Generics вирішують обидві проблеми. Нижче – мінімум теорії і чотири практичні приклади, які реально просять пояснити на співбесідах.

## Проблема, яку вирішують generics

Уяви функцію, що обгортає значення в масив:

```ts
function wrapInArray(value: any): any[] {
  return [value];
}

const numbers = wrapInArray(42);
numbers[0].toUpperCase(); // компілюється, падає в рантаймі
```

`any` тут «з'їв» тип: TypeScript більше нічого не перевіряє. Generic-версія зберігає зв'язок між входом і виходом:

```ts
function wrapInArray<T>(value: T): T[] {
  return [value];
}

const numbers = wrapInArray(42);      // number[]
numbers[0].toUpperCase();             // помилка компіляції – і це добре
```

`T` – параметр типу: він підставляється в момент виклику, найчастіше автоматично через inference. Це головна думка для співбесіди: **generic не «будь-який тип», а «конкретний тип, який ми дізнаємось пізніше»**.

## Constraints: extends і keyof

Без обмежень `T` може бути чим завгодно, тому всередині функції з ним майже нічого не можна робити. `extends` звужує допустимі типи:

```ts
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` зв'язує ключ з об'єктом:

```ts
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:

```ts
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 одночасно:

```ts
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 найчастіше трапляються не в утилітах, а в компонентах. Типовий приклад – список, який працює з будь-яким типом елементів:

```tsx
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-типи:

```ts
// Спрощені визначення з 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](https://www.typescriptlang.org/docs/handbook/2/generics.html).

## Куди далі

Generics найчастіше перевіряють у зв'язці з практикою: на [live-coding задачах із JavaScript](/blog/javascript-live-coding-zadachi) і [React-задачах із TypeScript](/blog/react-live-coding-typescript) типізація – половина оцінки. Загальну картину питань співбесіди дивись у [гайді з JavaScript-співбесіди](/blog/javascript-interview-guide).

Практичне завдання: напиши функцію `groupBy<T, K extends keyof T>(items: T[], key: K): Map<T[K], T[]>` без підглядання, а потім перевір типи на масиві об'єктів із різними полями. Якщо компілятор зловив хоч одну твою помилку – generics уже окупились.
