Skip to main content

Generic без extends

1. Що означає "generic без extends"

Коли ти оголошуєш generic ось так:

javascript
function identity<T>(value: T): T { return value; }
  • ти кажеш TypeScript:

"T - може бути абсолютно будь-яким типом."

І це справді так: T може бути числом, рядком, об'єктом, масивом, функцією, null, undefined, void, never, чим завгодно.


2. У чому проблема?

Проблема не в самому generic, а в припущеннях, які розробник робить про тип T.

Подивимось на приклад:

javascript
function getLength<T>(value: T): number { return value.length; // Помилка: у T може не бути length! }

TypeScript лається, бо T може бути чим завгодно - навіть number або boolean, і тоді length не існує.


Чому це небезпечно

Якби TypeScript не перевіряв це, ти міг би написати такий код:

javascript
function unsafeLength<T>(value: T): number { return (value as any).length; } console.log(unsafeLength(123)); // undefined console.log(unsafeLength(true)); // undefined

Код компілюється, але поведінка непередбачувана - runtime-помилки гарантовані. TypeScript більше тебе не захищає.


3. Як це виправити - додати extends

Ти можеш обмежити generic, щоб він підходив лише під певні типи:

javascript
function getLength<T extends { length: number }>(value: T): number { return value.length; // безпечно }

Тепер T повинен бути об'єктом (або масивом, рядком тощо), у якого гарантовано є властивість length.

Працює:

javascript
getLength("hello"); // 5 getLength([1, 2, 3]); // 3

Не працює:

javascript
getLength(42); // Помилка - number не має length

Завдяки extends TypeScript знає, які властивості можна використовувати.


4. Приклад небезпечного generic без extends

javascript
function merge<T, U>(a: T, b: U): T & U { return { ...a, ...b }; } merge(1, "test"); // Логічно безглуздо, але TS не забороняє

TypeScript не знає, що T і U мають бути об'єктами - і дозволяє передати числа, рядки і навіть null.


Безпечний варіант:

javascript
function merge<T extends object, U extends object>(a: T, b: U): T & U { return { ...a, ...b }; } merge({ name: "Tim" }, { age: 25 }); // OK merge(1, "test"); // Помилка - не об'єкт

Тепер TypeScript строго стежить за структурою даних.


5. Ще приклад - з ключами

Без extends можна легко "зламати" типову логіку:

javascript
function getValue<T, K>(obj: T, key: K) { return obj[key]; // Помилка: у T може не бути властивості K }

Правильно - обмежити K:

javascript
function getValue<T, K extends keyof T>(obj: T, key: K) { return obj[key]; // безпечно } getValue({ name: "Tim", age: 25 }, "name"); // OK getValue({ name: "Tim" }, "invalid"); // Помилка - немає такого ключа

Завдяки extends keyof T TypeScript знає, що key повинен бути наявною властивістю об'єкта.


6. Чому це важливо

Якщо generic не обмежений (T без extends):

  • ти не можеш безпечно використовувати властивості чи методи цього типу;
  • TypeScript не знає, які операції допустимі;
  • з'являються спокуси "затикати" типи через as any, що робить код небезпечним.

7. Правило

СитуаціяЩо робити
Використовуєш generic, але не звертаєшся до властивостейfunction identity<T>(x: T): T - нормально
Використовуєш властивості типу (.length, .id тощо)Додай extends { length: number }
Використовуєш ключі об'єктаДодай K extends keyof T
Очікуєш, що generic - об'єктДодай extends object
Хочеш обмежити до нащадків інтерфейсуДодай extends MyInterface

Підсумок

Без extendsЗ extends
T може бути будь-яким типомT обмежений певною структурою
Компілятор не знає, які властивості єКомпілятор перевіряє допустимі властивості
Можуть бути runtime-помилкиПомилки ловляться на етапі компіляції
Часто потребує as anyМайже не потребує as
Менш передбачуваний кодБезпечний і самодокументований код

Простіше кажучи:

Generic без extends - це "універсальний контейнер без обмежень". Без обмежень TypeScript не може тебе захистити.

Додавай extends, коли generic повинен задовольняти певному контракту (структурі, інтерфейсу чи ключам).

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.