Skip to main content

Перевантаження функцій у TypeScript

Перевантаження функцій (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 для обох викликів і змушувала б калерів перевіряти тип результату зайвий раз.

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

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

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

Дочитали статтю?