Перегрузка функций (function overloads)
Как одна функция может принимать разные типы аргументов и возвращать разные результаты — несколько сигнатур для одной реализации.
🎯 Что такое перегрузка функций?
Перегрузка функций (function overloads) — это возможность создать несколько сигнатур для одной функции. Это означает, что функция может принимать разные комбинации аргументов и возвращать разные типы значений, в зависимости от того, как она вызывается.
Представьте функцию поиска: она может искать по ID (и вернуть объект), по имени (и вернуть массив), или по regex (и вернуть совпадения). В JavaScript это решается через проверку типовво время выполнения, но TypeScript позволяет описать все варианты статически.
💡 Аналогия: Перегрузка — как меню ресторана. Одно блюдо «Паста» может быть с разными соусами. Повар (реализация) умеет готовить все варианты, но вы (вызывающий код) заказываете конкретный вариант, и получаете соответствующий результат.
📝 Синтаксис перегрузки функций
В TypeScript перегрузка функций реализуется через несколько сигнатур перед основной реализацией. Каждая сигнатура описывает один вариант вызова.
Базовый синтаксис
// Сигнатуры перегрузки (объявления)
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;
// Реализация (единственная)
function format(value: string | number | Date): string {
if (typeof value === "string") {
return value.trim();
}
if (typeof value === "number") {
return value.toFixed(2);
}
return value.toLocaleDateString("ru-RU");
}
// Вызовы — TypeScript знает, какой тип вернётся
const a = format(" hello "); // a: string (из сигнатуры string → string)
const b = format(42.5); // b: string (из сигнатуры number → string)
const c = format(new Date()); // c: string (из сигнатуры Date → string)
❌ Важно:
Сигнатуры перегрузки не имеют тела. Они — только объявление. Тело функции только одно — общая реализация, которая обрабатывает все варианты.
// ❌ Неправильно — сигнатуры не должны иметь тело
function format(value: string): string {
return value.trim(); // Эта сигнатура — не объявление перегрузки!
}
function format(value: number): string {
return value.toFixed(2); // Это уже другая функция — конфликт имён
}
// ✅ Правильно — сигнатуры без тела, одна реализация
function format(value: string): string; // Сигнатура 1
function format(value: number): string; // Сигнатура 2
function format(value: string | number): string { // Реализация
if (typeof value === "string") {
return value.trim();
}
return value.toFixed(2);
}
🔄 Перегрузка с разными типами возврата
Самая сильная сторона перегрузки — возможность возвращать разные типы в зависимости от входных данных.
Пример 1: Поиск элемента
// Сигнатуры: разные типы аргументов → разные типы результата
function findItem(id: number): { id: number; name: string } | undefined;
function findItem(name: string): { id: number; name: string }[];
function findItem(predicate: (item: { id: number; name: string }) => boolean): { id: number; name: string } | undefined;
// Реализация
function findItem(
query: number | string | ((item: { id: number; name: string }) => boolean)
): { id: number; name: string } | { id: number; name: string }[] | undefined {
const items = [
{ id: 1, name: "Ноутбук" },
{ id: 2, name: "Телефон" },
{ id: 3, name: "Планшет" }
];
if (typeof query === "number") {
return items.find(item => item.id === query);
}
if (typeof query === "string") {
return items.filter(item => item.name.includes(query));
}
return items.find(query);
}
// Вызовы — TypeScript точно знает тип результата
const byId = findItem(1);
// Тип: { id: number; name: string } | undefined
console.log(byId?.name); // "Ноутбук"
const byName = findItem("Тел");
// Тип: { id: number; name: string }[]
console.log(byName.length); // 1
const byPredicate = findItem(item => item.id > 2);
// Тип: { id: number; name: string } | undefined
console.log(byPredicate?.name); // "Планшет"
Пример 2: Конвертация типов
// Функция конвертации: в зависимости от второго аргумента возвращает разный тип
function convert(value: string, to: "number"): number;
function convert(value: number, to: "string"): string;
function convert(value: string, to: "date"): Date;
function convert(value: unknown, to: string): number | string | Date {
switch (to) {
case "number":
return Number(value);
case "string":
return String(value);
case "date":
return new Date(String(value));
default:
throw new Error("Unknown conversion type");
}
}
// Вызовы — тип результата зависит от второго аргумента
const num = convert("42", "number"); // num: number
const str = convert(42, "string"); // str: string
const date = convert("2025-01-01", "date"); // date: Date
console.log(num + 1); // 43
console.log(str.length); // 2
console.log(date.getFullYear()); // 2025
❓ Перегрузка с необязательными параметрами
Перегрузка позволяет комбинировать обязательные и необязательные параметры, создавая гибкие API.
// Создание объекта: разное количество параметров
function createPoint(x: number): { x: number; y: number; z: number };
function createPoint(x: number, y: number): { x: number; y: number; z: number };
function createPoint(x: number, y: number, z: number): { x: number; y: number; z: number };
function createPoint(
x: number,
y?: number,
z?: number
): { x: number; y: number; z: number } {
return { x, y: y ?? 0, z: z ?? 0 };
}
// Вызовы с разным числом аргументов
const p1 = createPoint(5);
// { x: 5, y: 0, z: 0 }
const p2 = createPoint(5, 10);
// { x: 5, y: 10, z: 0 }
const p3 = createPoint(5, 10, 15);
// { x: 5, y: 10, z: 15 }
Пример: отправка уведомлений
// Уведомление: разные способы вызова
function notify(message: string): void;
function notify(userId: string, message: string): void;
function notify(userIds: string[], message: string, options?: {
priority?: "low" | "normal" | "high";
channel?: "email" | "sms" | "push";
}): void;
function notify(
first: string | string[],
second?: string,
options?: { priority?: "low" | "normal" | "high"; channel?: "email" | "sms" | "push" }
): void {
if (typeof first === "string" && !second) {
// Один аргумент — общий broadcast
console.log(`[GLOBAL] ${first}`);
} else if (typeof first === "string" && second) {
// Два аргумента — конкретному пользователю
console.log(`[TO: ${first}] ${second}`);
} else if (Array.isArray(first) && second) {
// Массив пользователей + опции
const { priority = "normal", channel = "push" } = options || {};
console.log(`[TO: ${first.join(", ")}] [${priority}] [${channel}] ${second}`);
}
}
// Вызовы
notify("Система обновлена"); // [GLOBAL] Система обновлена
notify("user123", "Ваш заказ готов"); // [TO: user123] Ваш заказ готов
notify(
["user1", "user2", "user3"],
"Важное объявление",
{ priority: "high", channel: "email" }
);
// [TO: user1, user2, user3] [high] [email] Важное объявление
⚖️ Перегрузка vs union-типы: когда что использовать
Часто одну и ту же задачу можно решить двумя способами: через перегрузку или через union-типы. Давайте сравним.
Способ 1: Union-типы (без перегрузки)
function getLength(input: string | any[]): number {
return input.length;
}
// Проблема: TypeScript не знает точный тип на этапе вызова
const len1 = getLength("hello"); // len1: number ✅
const len2 = getLength([1, 2, 3]); // len2: number ✅
// Но если нужна разная логика в зависимости от типа — нужна проверкаво время выполнения
function process(input: string | number): string {
if (typeof input === "string") {
return input.toUpperCase(); // string
}
return input.toFixed(2); // number → string
}
Способ 2: Перегрузка функций
// Перегрузка: тип возврата зависит от типа аргумента
function process(input: string): string;
function process(input: number): string;
function process(input: string | number): string {
if (typeof input === "string") {
return input.toUpperCase();
}
return input.toFixed(2);
}
// TypeScript точно знает тип
const s = process("hello"); // s: string (из сигнатуры string → string)
const n = process(42); // n: string (из сигнатуры number → string)
| Критерий | Union-типы | Перегрузка |
|---|---|---|
| Тип возврата одинаковый | ✅ Подходит | Избыточно |
| Тип возврата разный | ❌ Не может | ✅ Подходит |
| Количество аргументов разное | ❌ Сложно | ✅ Подходит |
| Простота кода | Проще | Сложнее |
| Читаемость для потребителя | Средняя | Высокая |
🛠️ Реальные примеры перегрузки
Пример 1: Селектор элементов
// Подобно document.querySelector — разные типы аргументов
function select(selector: string): HTMLElement | null;
function select(selector: string, parent: HTMLElement): HTMLElement | null;
function select(
tagName: K
): HTMLElementTagNameMap[K] | null;
function select(
selectorOrTag: string,
parentOrUndefined?: HTMLElement
): HTMLElement | null {
if (parentOrUndefined) {
return parentOrUndefined.querySelector(selectorOrTag);
}
return document.querySelector(selectorOrTag);
}
// Вызовы
const div = select("div"); // HTMLElement | null
const btn = select("button", someParent); // HTMLElement | null
const header = select("header"); // HTMLElementTagNameMap["header"] | null
Пример 2: Форматирование
// Форматирование: принимает разные типы, возвращает строку
function format(template: string, ...args: unknown[]): string;
function format(obj: Record): string;
function format(
templateOrObj: string | Record,
...args: unknown[]
): string {
if (typeof templateOrObj === "string") {
let result = templateOrObj;
args.forEach((arg, index) => {
result = result.replace(`{${index}}`, String(arg));
});
return result;
}
return Object.entries(templateOrObj)
.map(([key, value]) => `${key}: ${value}`)
.join(", ");
}
// Вызовы
const msg1 = format("Привет, {0}! Вам {1} лет.", "Анна", 25);
// "Привет, Анна! Вам 25 лет."
const msg2 = format({ name: "Анна", age: 25, city: "Москва" });
// "name: Анна, age: 25, city: Москва"
Пример 3: Загрузка данных
// Загрузка: разные типы URL возвращают разные типы данных
function fetchData(url: "/api/users"): Promise<{ id: string; name: string }[]>;
function fetchData(url: `/api/users/${string}`): Promise<{ id: string; name: string }>;
function fetchData(url: "/api/products"): Promise<{ id: string; price: number }[]>;
function fetchData(url: string): Promise;
async function fetchData(url: string): Promise {
const response = await fetch(url);
return response.json();
}
// Вызовы — тип возвращаемого значения зависит от URL
const users = await fetchData("/api/users");
// users: { id: string; name: string }[]
const user = await fetchData("/api/users/123");
// user: { id: string; name: string }
const products = await fetchData("/api/products");
// products: { id: string; price: number }[]
// TypeScript не позволит передать неправильный URL без общей сигнатуры
const unknown = await fetchData("/api/unknown");
// unknown: unknown
🚫 Когда НЕ нужно перегружать
Перегрузка — мощный инструмент, но его не стоит использовать везде. Вот когда лучше обойтись без неё:
Когда union-тип проще и sufficient
// ❌ Избыточная перегрузка — проще использовать union
function getId(id: number): number;
function getId(id: string): number;
function getId(id: number | string): number {
if (typeof id === "string") {
return parseInt(id, 10);
}
return id;
}
// ✅ Проще — union-тип + default implementation
function getId(id: number | string): number {
if (typeof id === "string") {
return parseInt(id, 10);
}
return id;
}
Когда результат одинаковый
// ❌ Если тип возврата одинаковый — перегрузка не нужна
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: number | string, b: number | string): number | string {
if (typeof a === "number" && typeof b === "number") {
return a + b;
}
return String(a) + String(b);
}
// ✅ Дженерик или union — проще
function add(a: T, b: T): T {
if (typeof a === "number") {
return (a + b) as T;
}
return (String(a) + String(b)) as T;
}
Когда слишком много сигнатур
// ❌ Слишком много перегрузок — код становится нечитаемым
function process(a: string): string;
function process(a: string, b: number): string;
function process(a: string, b: number, c: boolean): string;
function process(a: number): number;
function process(a: number, b: string): number;
function process(a: number, b: string, c: boolean): number;
function process(a: string | number, b?: number | string, c?: boolean): string | number {
// Сложная логика...
}
// ✅ Лучше — объект параметров
interface ProcessOptions {
value: string | number;
count?: number;
label?: string;
verbose?: boolean;
}
function process(options: ProcessOptions): string | number {
const { value, count, label, verbose } = options;
// Проще и понятнее
}
❌ Частые ошибки
Ошибка 1: Тело функции в сигнатуре перегрузки
// ❌ Ошибка — сигнатура перегрузки не должна иметь тела
function greet(name: string): string {
return "Привет, " + name; // Это уже реализация!
}
// ✅ Правильно — сигнатуры без тела
function greet(name: string): string; // Сигнатура
function greet(name: string, age: number): string; // Сигнатура
function greet(name: string, age?: number): string { // Реализация
if (age) {
return `Привет, ${name}! Вам ${age} лет.`;
}
return `Привет, ${name}!`;
}
Ошибка 2: Реализация не покрывает все сигнатуры
// ❌ Сигнатура обещает number, но реализация может вернуть string
function parse(value: string): number;
function parse(value: string): string;
function parse(value: string): number | string {
if (value.startsWith("#")) {
return value; // string — не покрывает первую сигнатуру!
}
return parseInt(value, 10);
}
// ✅ Правильно — реализация должна покрывать ВСЕ сигнатуры
function parse(value: string): number;
function parse(value: string, radix: number): number;
function parse(value: string, radix: number = 10): number {
return parseInt(value, radix);
}
Ошибка 3: Порядок сигнатур
// ⚠️ Более специфичные сигнатуры должны идти раньше
function find(query: RegExp): string[];
function find(query: string): string; // ← Сначала строки
function find(query: string, options?: object): string[]; // ← Потом с опциями
// Если поставить менее специфичную первую — TypeScript будет матчить первую попавшуюся
function find(query: string): string;
function find(query: RegExp): string[];
function find(query: string | RegExp): string | string[] {
// ...
}
📋 Итоги урока
- Перегрузка — несколько сигнатур (объявлений) + одна реализация
- Сигнатуры не имеют тела — только описание типов
- Реализация единственная — обрабатывает все варианты
- Перегрузка vs union: перегрузка нужна когда тип возврата зависит от типа аргумента
- Не перегружайте если union-типа достаточно — проще и понятнее
- Порядок: более специфичные сигнатуры идут раньше
✅ Ключевое правило: Перегрузка — это когда одна функция ведёт себя по-разному в зависимости от типов аргументов. Используйте её, когда нужно, чтобы TypeScript знал точный тип возвратного значения для каждого варианта вызова.
В следующем уроке мы изучим стрелочные функции и их типизацию — более компактный способ создания функций в TypeScript.
Проверяем понимание
6 вопросов