$ sudo teach IT
МОДУЛЬ 4 · УРОК 4

Перегрузка функций (function overloads)

Как одна функция может принимать разные типы аргументов и возвращать разные результаты — несколько сигнатур для одной реализации.

⏱ ~22 минут 🎓 Средний уровень 📌 Функции

🎯 Что такое перегрузка функций?

Перегрузка функций (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 вопросов

Перегрузка функции area

Premium