Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Перевантаження функцій у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Перевантаження функцій (function overloads)** - кілька сигнатур для однієї функції у TypeScript. Компілятор вибирає потрібну і призначає точний тип повернення залежно від переданих аргументів. ```typescript function parse(input: string): object; function parse(input: object): string; function parse(input: string | object): string | object { return typeof input === 'string' ? JSON.parse(input) : JSON.stringify(input); } parse('{"name":"Alice"}'); // object parse({ name: 'Alice' }); // string ``` **Ключове:** overload-сигнатури визначають публічний API. Сигнатура реалізації прихована від калерів.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Перевантаження функцій (function overloads)** у TypeScript дозволяє оголосити кілька сигнатур для однієї функції. Компілятор сам вибирає потрібну на основі аргументів, які ти передаєш. ## Теорія ### TL;DR - Уяви меню з двома варіантами одного блюда: передаєш один аргумент - отримуєш один тип; додаєш другий - TypeScript вибирає іншу сигнатуру автоматично. - Головна різниця з опціональними параметрами: кожне перевантаження задає точні типи для свого варіанту виклику, без `any` на call site. - Сигнатура реалізації прихована від зовнішнього коду. Калери бачать лише overload-сигнатури. - Порядок важливий: специфічні сигнатури спочатку, широкі в кінці. - Нульова ціна в рантаймі: всі перевантаження стираються і залишається одна JS-функція. ### Швидкий приклад ```typescript function greet(name: string): string; // Перевантаження 1 function greet(first: string, last: string): string; // Перевантаження 2 function greet(first: string, last?: string): string { // Реалізація (не видима ззовні) return last ? `Hello, ${first} ${last}!` : `Hello, ${first}!`; } greet("Alice"); // string - перше перевантаження greet("Bob", "Lee"); // string - друге перевантаження // greet(42); // Помилка: жодне перевантаження не приймає number ``` Дві overload-сигнатури визначають, що можуть робити калери. Реалізація під ними обробляє обидва випадки через опціональний параметр. TypeScript ніколи не показує сигнатуру реалізації в автодоповненні або повідомленнях про помилки. ### Головна різниця Перевантаження дають точний тип повернення для кожної форми аргументів на етапі компіляції. Одна функція з union-типами змушує калерів мати справу з `string | number` навіть якщо вони передали конкретний літеральний аргумент. З overloads - передаєш `'name'` у `getValue` і отримуєш `string`, передаєш `'age'` і отримуєш `number`. Жодних type guard на стороні виклику. ### Коли використовувати - Тип повернення залежить від значення аргументу: overloads (наприклад, `getValue('name')` повертає `string`, `getValue('age')` повертає `number`) - Кількість аргументів змінює форму результату: overloads замість опціоналів - Пишеш бібліотеку або публічний API, де калерам потрібні точні типи - Функція обробляє два різних сценарії виклику (GET без body vs POST з body) - Всі типи повернення однакові і аргументи просто додаються: union з опціоналами простіший і зрозуміліший ### Як TypeScript вирішує перевантаження Компілятор переглядає overload-сигнатури зверху вниз і призначає тип повернення першого збігу. Це відбувається на етапі компіляції, не в рантаймі. У VS Code або будь-якому редакторі з LSP наведення на виклик показує розв'язане перевантаження, а не сигнатуру реалізації. ### Типові помилки **Широка сигнатура на першому місці** ```typescript // Неправильно: компілятор вибирає перший збіг - специфічні стають недосяжними function get(key: string): string | number; function get(key: 'age'): number; // Ніколи не буде вибрано // Правильно function get(key: 'age'): number; function get(key: string): string | number; function get(key: string): string | number { /* ... */ } ``` Якщо `string` стоїть перед `'age'`, кожен виклик матчить широку сигнатуру і літеральне перевантаження стає мертвим кодом. Це найпоширеніша помилка з overloads. **Розраховувати на захист у рантаймі** ```typescript function safeDiv(a: number, b: number): number; function safeDiv(a: number): number; function safeDiv(a: number, b?: number): number { return a / b; // b може бути undefined - результат NaN } safeDiv(10); // TypeScript задоволений, рантайм повертає NaN ``` Перевантаження існують лише в компайл-таймі. Захист пиши в тілі реалізації. **Опціональні параметри в overload-сигнатурах** ```typescript // Компілюється, але друге перевантаження вводить в оману function greet(name: string): string; function greet(first: string, last?: string): string { /* ... */ } // опціональний в overload ``` Overload-сигнатури повинні містити лише обов'язкові параметри. Опціональні - тільки в реалізації. **Перевантаження там де достатньо union** ```typescript // Тип повернення завжди string - overloads тут нічого не дають function format(value: string): string; function format(value: number): string; function format(value: string | number): string { return String(value); } // Просто пиши: function format(value: string | number): string { return String(value); } ``` ### Де зустрічається в реальному коді - React: `React.createElement` перевантажує по типу тегу для різних JSX-форм - Node.js: `fs.readFile` перевантажує варіанти з callback і Promise - Express: `res.json()` перевантажує object vs string - Lodash: `_.get()` перевантажує path як `string | string[] | number` - React hooks: `useState<string>('')` розв'язується через generic overload і дає точний тип `[string, Dispatch<SetStateAction<string>>]` З практики: реальна цінність перевантажень часто не в самому звуженні типів, а в IDE-ергономіці. Коли наводиш на виклик і бачиш точно `Promise<string>` замість `Promise<string | number>` - намір функції зрозумілий для будь-кого, хто читатиме код через пів року. ### Питання на співбесіді **Q:** Як TypeScript вирішує неоднозначні виклики при перевантаженнях? **A:** Компілятор бере першу підходящу сигнатуру в порядку оголошення. Якщо жодне перевантаження не підходить - помилка компіляції. Немає пошуку "найкращого збігу", є тільки "перший збіг". **Q:** У чому різниця між overloads і intersection-типами для параметрів функцій? **A:** Overloads розгалужуються на основі форми аргументів: різні аргументи дають різні типи повернення. Intersection-типи комбінують вимоги: обидва обмеження мають виконуватись одночасно. Overloads потрібні, коли функція поводиться по-різному залежно від вхідних даних. **Q:** Чи можуть перевантаження бути дженерічними? **A:** Так. `function id<T>(x: T): T;` - валідна overload-сигнатура. Генерічні overloads широко використовуються в React-хуках: `useState<string>('')` розв'язується через generic overload і дає точний кортеж `[string, Dispatch<SetStateAction<string>>]`. **Q:** Чи впливають перевантаження на розмір бандла або продуктивність? **A:** Ні. TypeScript стирає всі overload-сигнатури при компіляції. В емітованому JS залишається лише функція реалізації. **Q:** (Сеніор) Калер передає union-тип як аргумент. Яке перевантаження вибере TypeScript? **A:** TypeScript не розщеплює union автоматично для окремого пошуку серед overloads. Якщо передати `key: string | 'age'`, компілятор перевіряє цей union цілком проти кожної сигнатури. Найімовірніше ти отримаєш тип повернення широкого перевантаження. Це часте джерело плутанини при споживанні перевантажених API з похідними типами. ## Приклади ### Обгортка HTTP-запиту Типовий патерн в Express-додатках і внутрішніх API-клієнтах: типізовані хендлери де GET не приймає body, а POST вимагає його. ```typescript function request(method: 'GET', url: string): Promise<string>; function request(method: 'POST', url: string, body: object): Promise<number>; function request(method: string, url: string, body?: object): Promise<string | number> { if (method === 'GET') return Promise.resolve(`Fetched ${url}`); return Promise.resolve(Object.keys(body!).length); } const response = request('GET', '/users'); // Promise<string> const status = request('POST', '/users', {id: 1}); // Promise<number> // request('POST', '/users'); // Помилка: body обов'язковий для POST ``` TypeScript змушує передавати `body` при POST і блокує його при GET. Реалізація підтримує обидва варіанти через опціональний параметр, але зовні це не видно. ### Динамічний тип повернення за ключем Класичний патерн на співбесіді: функція повертає різні типи залежно від літерального рядкового аргументу. ```typescript function getValue(key: 'name'): string; function getValue(key: 'age'): number; function getValue(key: 'active'): boolean; function getValue(key: string): string | number | boolean { const data = { name: 'Alice', age: 30, active: true }; return data[key as keyof typeof data]; } const name = getValue('name'); // string const age = getValue('age'); // number const active = getValue('active'); // boolean ``` Без перевантажень усі три виклики повертали б `string | number | boolean` і ти б витрачав час на type guard всюди де використовується результат. ### Витяг елементів масиву Генерічне перевантаження: кількість аргументів змінює форму результату. ```typescript function getFirst<T>(arr: T[]): T | undefined; function getFirst<T>(arr: T[], count: number): T[]; function getFirst<T>(arr: T[], count?: number): T | T[] | undefined { return count === undefined ? arr[0] : arr.slice(0, count); } const users = ['Alice', 'Bob', 'Carol']; const one = getFirst(users); // string | undefined const three = getFirst(users, 2); // string[] ``` Два перевантаження кажуть: без count можливо нічого не отримаєш (порожній масив), з count завжди отримуєш масив. Одна сигнатура з `count?: number` повертала б `string | string[] | undefined` для обох викликів і змушувала б калерів перевіряти тип результату зайвий раз.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.