Generic без extends
1. Що означає "generic без extends"
Коли ти оголошуєш generic ось так:
function identity<T>(value: T): T {
return value;
}- ти кажеш TypeScript:
"
T- може бути абсолютно будь-яким типом."
І це справді так:
T може бути числом, рядком, об'єктом, масивом, функцією, null, undefined, void, never, чим завгодно.
2. У чому проблема?
Проблема не в самому generic, а в припущеннях,
які розробник робить про тип T.
Подивимось на приклад:
function getLength<T>(value: T): number {
return value.length; // Помилка: у T може не бути length!
}TypeScript лається, бо T може бути чим завгодно - навіть number або boolean,
і тоді length не існує.
Чому це небезпечно
Якби TypeScript не перевіряв це, ти міг би написати такий код:
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, щоб він підходив лише під певні типи:
function getLength<T extends { length: number }>(value: T): number {
return value.length; // безпечно
}Тепер T повинен бути об'єктом (або масивом, рядком тощо),
у якого гарантовано є властивість length.
Працює:
getLength("hello"); // 5
getLength([1, 2, 3]); // 3Не працює:
getLength(42); // Помилка - number не має lengthЗавдяки
extendsTypeScript знає, які властивості можна використовувати.
4. Приклад небезпечного generic без extends
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b };
}
merge(1, "test"); // Логічно безглуздо, але TS не забороняєTypeScript не знає, що
TіUмають бути об'єктами - і дозволяє передати числа, рядки і навітьnull.
Безпечний варіант:
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 можна легко "зламати" типову логіку:
function getValue<T, K>(obj: T, key: K) {
return obj[key]; // Помилка: у T може не бути властивості K
}Правильно - обмежити K:
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 TTypeScript знає, що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 повинен задовольняти певному контракту (структурі, інтерфейсу чи ключам).
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.