Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке generic у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Generics** у TypeScript - це параметри типу (`<T>`), які дозволяють писати функції, класи та інтерфейси що працюють з будь-яким типом, зберігаючи повну перевірку типів. ```typescript function first<T>(arr: T[]): T | undefined { return arr[0]; } const num = first([1, 2, 3]); // number const str = first(['a', 'b']); // string ``` **Головне:** На відміну від `any`, generics передають конкретний тип далі, TypeScript перевіряє його на кожному виклику.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Generics** у TypeScript - це параметри типу, які дозволяють написати функцію, клас або інтерфейс один раз і перевикористовувати для будь-якого типу, поки TypeScript зберігає повну перевірку типів на кожному виклику. ## Теорія ### TL;DR - Generics - як форма для відливання: описуєш форму один раз (`<T>`), заповнюєш будь-яким типом при використанні, отримуєш повну перевірку для цього типу - Головна різниця: `any` знищує інформацію про тип; generics передають конкретний тип крізь весь ланцюжок - TypeScript сам виводить `T` з аргументів, явно писати `<string>` зазвичай не потрібно - Generics - коли одна логіка підходить для 2+ типів; конкретні типи - для однотипного коду - Обмеження `T extends object` звужують допустимі типи ### Швидкий приклад ```typescript // Без generics: безпека типів втрачається function first(arr: any[]): any { return arr[0]; } const val = first([1, 2, 3]); // val це 'any', без автодоповнення і перевірок // val.toUpperCase(); // TypeScript мовчить, але впаде в runtime // З generics: тип проходить крізь функцію function first<T>(arr: T[]): T | undefined { return arr[0]; } const num = first([1, 2, 3]); // num це number const str = first(['a', 'b']); // str це string // num.toUpperCase(); // Помилка компіляції, TypeScript це зловить ``` `T` виводиться з аргументу автоматично. Писати `first<number>([1, 2, 3])` явно зазвичай не потрібно. ### Ключова різниця Різниця між generics і `any` не просто стилістична. З `any` TypeScript взагалі припиняє перевіряти типи: немає автодоповнення, немає виявлення помилок. З generics TypeScript підставляє конкретний тип на кожному виклику. Пишеш `first([1, 2, 3])` і TypeScript вважає кожне `T` у функції рівним `number`. Тип передається далі, а не зникає. ### Коли використовувати - Одна логіка для різних типів (операції з масивами, обгортки даних, хелпери для API): generics - Тільки один конкретний тип: використовуй цей тип напряму - Кілька типів на вході, однакова структура на виході: generics з обмеженнями (`T extends {id: string}`) - Спільні утиліти по кодовій базі: generics (кожен модуль підставляє свій тип) - Всього 2-3 відомі варіанти: перевантаження (overloads) можуть бути простішими ### Як TypeScript обробляє generics TypeScript розглядає `T` як абстрактний плейсхолдер під час оголошення, потім виконує підстановку типу на кожному виклику. Все це відбувається під час компіляції. В емітованому JavaScript від generics не залишається і сліду. Для `T extends string` компілятор звужує допустимі підстановки ще до генерації JS. Жодних витрат у runtime. ### Типові помилки **Помилка: мутація generic-аргументу як ніби він `any`** ```typescript // Неправильно function append<T>(arr: T[]): T[] { arr.push('extra' as any); // Ламає T[] у того хто викликав return arr; } // Правильно function append<T>(arr: T[], item: T): T[] { return [...arr, item]; // Тип контролює той, хто викликає } ``` `T` зафіксований на момент виклику. Додавання `string` у `T[]` де `T` це `number` зламає код, навіть якщо TypeScript пропустив це за cast. **Помилка: забути обмеження для операцій з об'єктами** ```typescript // Неправильно, T може бути примітивом і spread не спрацює function merge<T>(a: T, b: T) { return { ...a, ...b }; } // Правильно function merge<T extends object>(a: T, b: T) { return { ...a, ...b }; } ``` **Помилка: немає аргументів для виведення типу** ```typescript // Неправильно, T виводиться як never[] function createArray<T>(): T[] { return []; } const arr = createArray(); // never[] // Правильно, аргумент допомагає вивести тип function createArray<T>(size: number, fill: T): T[] { return Array(size).fill(fill); } const nums = createArray(3, 0); // number[] ``` ### Де зустрічається в реальних проектах - React: `useRef<HTMLDivElement>()` повертає `RefObject<HTMLDivElement>`, точний тип DOM-елемента - React state: `useState<User | null>(null)` типізований з самого початку, не `any` - TanStack Query: `useQuery<User[]>` передає типи у всі селектори - Lodash типи: `_.map<T, R>(arr: T[], fn: (x: T) => R): R[]` пов'язує типи входу і виходу - Express: `Request<P, ResBody, ReqBody>` для типізованих обробників маршрутів ### Питання на співбесіді **Q:** Яка різниця між `T extends object` і `T extends {}`? **A:** `T extends {}` підходить для всього крім `null` і `undefined`, включно з примітивами. `T extends object` також виключає примітиви. Для spread-операцій потрібен саме `T extends object`. **Q:** Напиши функцію яка вибирає властивості об'єкта за масивом ключів. **A:** ```typescript function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { return keys.reduce((acc, k) => ({ ...acc, [k]: obj[k] }), {} as Pick<T, K>); } ``` `K extends keyof T` гарантує що передаються тільки валідні ключі. TypeScript зловить `pick(user, ['salary'])` якщо `salary` не є частиною типу. **Q:** Чим TypeScript generics відрізняються від Java generics під час виконання? **A:** TypeScript generics повністю стираються під час компіляції, у JS-виводі немає жодної інформації про типи. В Java в деяких випадках є reified generics, що дозволяють перевірки в runtime. В TypeScript це неможливо. **Q:** Що робить `infer` в умовних типах? **A:** ```typescript type UnpackPromise<T> = T extends Promise<infer U> ? U : T; // UnpackPromise<Promise<string>> = string // UnpackPromise<number> = number ``` `infer U` каже TypeScript: витягни внутрішній тип із `Promise<...>` і запиши його в `U` для гілки true. Саме так побудований вбудований тип `Awaited<T>`. **Q:** Junior vs senior на bounded generics: обмеж тип тільки рядками і числами. **A:** Junior пише `T extends string | number` і це працює. Senior знає межі: якщо потрібна різна поведінка залежно від типу, перевантаження або умовні типи (`T extends string ? string : number`) дають більше контролю. ## Приклади ### Базовий: identity-функція з виведенням типу ```typescript function identity<T>(arg: T): T { return arg; } const num = identity(42); // T виводиться як number const str = identity('hello'); // T виводиться як string const obj = identity({ id: 1 }); // T виводиться як { id: number } // num.toUpperCase(); // Помилка компіляції, TypeScript знає що це number console.log(num.toFixed(2)); // "42.00" ``` TypeScript сам визначає `T` з аргументу. Явно писати `identity<number>(42)` не потрібно. ### Середній: типізований доступ до властивостей з двома параметрами типу ```typescript function getProp<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } interface Person { name: string; age: number; } const person: Person = { name: 'Alice', age: 30 }; console.log(getProp(person, 'name')); // "Alice", тип результату string console.log(getProp(person, 'age')); // 30, тип результату number // getProp(person, 'salary'); // Помилка компіляції: 'salary' немає в Person ``` Два параметри типу працюють разом: `T` це об'єкт, `K` обмежений ключами `T`. Тип результату `T[K]` це точний тип тієї властивості. Жодного приведення типів. ### Просунутий: generic-хук для отримання даних ```typescript // Патерн з TanStack Query та власних fetch-оберток function useData<T>(url: string): { data: T | null; loading: boolean } { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch(url) .then(res => res.json()) .then(setData) .finally(() => setLoading(false)); }, [url]); return { data, loading }; } interface User { id: number; name: string; } // T = User[], тому data це User[] | null const { data: users, loading } = useData<User[]>('/api/users'); if (users) { console.log(users[0].name); // Безпечно, TypeScript знає структуру } ``` Сам хук нічого не знає про `User`. Тип вибирає той, хто викликає. В кожному проекті де я писав власну fetch-обгортку, ми врешті-решт приходили саме до цього патерну: варто додати один `<T>` і все дерево компонентів отримує типізацію автоматично.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.