Перегрузка функций в TypeScript: гайд 2025 с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

TypeScript Function Overloading: почему это не «магия», а инструмент для умного кода

Если вы когда-нибудь писали на JavaScript функцию, которая принимает «то строку, то число, а то и массив», вы знаете этот неприятный момент: внутри функции — ад из if, а снаружи — полная неопределённость для того, кто эту функцию вызывает. TypeScript решает эту проблему элегантно, но не всегда очевидно. Перегрузка функций (function overloading) — это не про создание нескольких функций с одинаковым именем (как в Java или C++). Это про объявление нескольких сигнатур для одной реализации.

В 2025 году, когда TypeScript стал стандартом де-факто для серьёзных фронтенд- и бэкенд-проектов, умение грамотно настраивать перегрузку — это не «фишка», а навык, который делает ваш код читаемым, типобезопасным и удобным для команды. Давайте разберёмся, как это работает, на чём ломаются новички и какие паттерны стоит взять на вооружение.

Что такое перегрузка функций на самом деле?

Представьте, что вы пишете функцию formatDate. Она должна уметь:

  • Принять объект Date и вернуть строку.
  • Принять строку в формате ISO и вернуть строку.
  • Принять число (timestamp) и вернуть строку.

Если просто написать function formatDate(input: Date | string | number): string, TypeScript не сможет подсказать, какой именно тип вы передаёте в конкретном вызове. IDE покажет общий тип, и вы потеряете автодополнение. Перегрузка решает это:

// Сигнатуры перегрузки (overload signatures)
function formatDate(input: Date): string;
function formatDate(input: string): string;
function formatDate(input: number): string;

// Реализация (implementation signature)
function formatDate(input: Date | string | number): string {
    if (input instanceof Date) {
        return input.toISOString();
    } else if (typeof input === 'string') {
        return new Date(input).toISOString();
    } else {
        return new Date(input).toISOString();
    }
}

Теперь, когда вы вызываете formatDate(new Date()), TypeScript знает, что возвращается string, и подсказывает типы. Магии нет — есть строгая типизация.

Правило №1: сигнатуры перегрузки не могут быть реализацией

Самая частая ошибка новичков — пытаться написать тело функции в каждой сигнатуре. Не делайте так:

// ❌ Ошибка: дублирование реализации
function foo(x: number): number {
    return x * 2;
}
function foo(x: string): string {
    return x.toUpperCase();
}

TypeScript не поддерживает множественные реализации с одинаковым именем. Всегда одна реализация, несколько объявлений.

Правило №2: порядок сигнатур имеет значение

TypeScript проверяет сигнатуры сверху вниз. Первая подходящая сигнатура определяет возвращаемый тип. Поэтому:

  • Более конкретные типы ставьте выше (например, string перед any).
  • Избегайте пересекающихся сигнатур, которые могут сбить компилятор.
// Хорошо: сначала конкретный случай
function createButton(label: string, disabled?: boolean): HTMLButtonElement;
function createButton(options: { label: string; disabled?: boolean }): HTMLButtonElement;
function createButton(labelOrOptions: string | { label: string; disabled?: boolean }, disabled?: boolean): HTMLButtonElement {
    // реализация
}

Правило №3: используйте Union Types, если сигнатур больше 3

Перегрузка — мощный инструмент, но не злоупотребляйте им. Если у вас 5+ сигнатур, код становится нечитаемым. В 2025 году хорошей практикой считается:

  • До 3 сигнатур — используйте перегрузку.
  • Больше 3 — подумайте о Union Type или объекте-конфигурации.
// Вместо 4 перегрузок:
function fetchData(url: string): Promise<Response>;
function fetchData(url: string, method: 'GET'): Promise<Response>;
function fetchData(url: string, method: 'POST', body: unknown): Promise<Response>;
function fetchData(url: string, method: 'PUT', body: unknown): Promise<Response>;
// ...лучше так:
type FetchOptions = {
    method: 'GET' | 'POST' | 'PUT';
    body?: unknown;
};
function fetchData(url: string, options?: FetchOptions): Promise<Response>;

Типичные грабли 2025: что пошло не так?

Вот несколько ситуаций, которые мы регулярно видим в коде на Stack Overflow и в реальных проектах:

1. Забыли сделать реализацию совместимой со всеми сигнатурами

Реализация должна быть «супертипом» для всех сигнатур. Если сигнатура принимает string, а реализация — только number, TypeScript выдаст ошибку.

// ❌ Ошибка: реализация несовместима
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: number, b: number): number { // не хватает string
    return a + b;
}

2. Перегрузка с опциональными параметрами

Если параметр опционален в одной сигнатуре, но обязателен в другой — будьте аккуратны с порядком и типами.

// ✅ Правильно: используем union для опциональности
function greet(name: string): string;
function greet(name: string, greeting?: string): string;
function greet(name: string, greeting?: string): string {
    return greeting ? `${greeting}, ${name}` : `Hello, ${name}`;
}

Продвинутый паттерн: перегрузка с дженериками

В 2025 году редко кто пишет перегрузки без дженериков. Они позволяют сохранить связь между типом входа и выхода. Например, функция, которая принимает массив или одиночное значение и возвращает соответствующий тип:

// Сигнатуры с дженериками
function wrap<T>(value: T): T[];
function wrap<T>(value: T[]): T[][];
function wrap<T>(value: T | T[]): T[] | T[][] {
    return Array.isArray(value) ? [value] : [value];
}

// Использование:
const a = wrap('hello');    // тип: string[]
const b = wrap([1, 2, 3]); // тип: number[][]

Это мощный инструмент для библиотек и API, где нужно сохранить точность типов без потери гибкости.

Когда перегрузка НЕ нужна: альтернативы 2025

Не каждая функция с разными аргументами требует перегрузки. Рассмотрите:

  • Объект-конфигурация — для функций с 3+ параметрами.
  • Rest parameters — если количество аргументов неизвестно.
  • Условные типы (Conditional Types) — для сложной логики на уровне типов.
// Вместо перегрузки: используем условный тип
type Return                

Блоги

Рекомендаций по книгам