Перевантаження функцій у TypeScript
Перевантаження функцій (function overloads) у TypeScript дозволяє оголосити кілька сигнатур для однієї функції. Компілятор сам вибирає потрібну на основі аргументів, які ти передаєш.
Теорія
TL;DR
- Уяви меню з двома варіантами одного блюда: передаєш один аргумент - отримуєш один тип; додаєш другий - TypeScript вибирає іншу сигнатуру автоматично.
- Головна різниця з опціональними параметрами: кожне перевантаження задає точні типи для свого варіанту виклику, без
anyна call site. - Сигнатура реалізації прихована від зовнішнього коду. Калери бачать лише overload-сигнатури.
- Порядок важливий: специфічні сигнатури спочатку, широкі в кінці.
- Нульова ціна в рантаймі: всі перевантаження стираються і залишається одна JS-функція.
Швидкий приклад
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 наведення на виклик показує розв'язане перевантаження, а не сигнатуру реалізації.
Типові помилки
Широка сигнатура на першому місці
// Неправильно: компілятор вибирає перший збіг - специфічні стають недосяжними
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.
Розраховувати на захист у рантаймі
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-сигнатурах
// Компілюється, але друге перевантаження вводить в оману
function greet(name: string): string;
function greet(first: string, last?: string): string { /* ... */ } // опціональний в overloadOverload-сигнатури повинні містити лише обов'язкові параметри. Опціональні - тільки в реалізації.
Перевантаження там де достатньо union
// Тип повернення завжди 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 вимагає його.
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 обов'язковий для POSTTypeScript змушує передавати body при POST і блокує його при GET. Реалізація підтримує обидва варіанти через опціональний параметр, але зовні це не видно.
Динамічний тип повернення за ключем
Класичний патерн на співбесіді: функція повертає різні типи залежно від літерального рядкового аргументу.
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 всюди де використовується результат.
Витяг елементів масиву
Генерічне перевантаження: кількість аргументів змінює форму результату.
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 для обох викликів і змушувала б калерів перевіряти тип результату зайвий раз.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.