Avatar ·

Перегрузка функций в TypeScript: как правильно объявить и использовать?

Всем привет! Я только начал изучать TypeScript после JavaScript и столкнулся с непонятной для меня темой — перегрузка функций (function overloading). В JavaScript я привык писать функции, которые могут принимать разные типы аргументов, и проверять их внутри с помощью `typeof` или `instanceof`. Например, у меня есть функция, которая должна принимать либо строку, либо число, и в зависимости от этого делать разные операции. В JS я делал так: ```typescript function format(input) { if (typeof input === 'string') { return input.trim(); } else if (typeof input === 'number') { return input.toFixed(2); } } ``` Но в TypeScript я хочу описать типы для этой функции. Я попробовал написать: ```typescript function format(input: string | number): string { // ... } ``` Это работает, но я слышал, что в TypeScript есть "настоящая" перегрузка функций, как в C++ или Java. Я попытался сделать так: ```typescript function format(input: string): string; function format(input: number): string; function format(input: string | number): string { // реализация } ``` Но у меня возникли вопросы: 1. Зачем нужна эта сигнатура перегрузки, если можно просто использовать union type? 2. Почему сигнатура реализации должна быть более общей, чем сигнатуры перегрузки? 3. Можно ли перегружать функции с разным количеством параметров? Объясните, пожалуйста, на простых примерах, как правильно использовать перегрузку функций в TypeScript и в каких случаях это действительно нужно, а когда достаточно union types или дженериков. Заранее спасибо! <<>>

Что такое перегрузка функций в TypeScript и зачем она нужна?

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

Ключевое отличие от union type

Рассмотрим ваш пример с union type:

function format(input: string | number): string {
  if (typeof input === 'string') return input.trim();
  return input.toFixed(2);
}

const result = format(42); // result: string

Проблема: TypeScript всегда выводит string как возвращаемый тип, даже если вы передали число. А что если мы хотим, чтобы тип возвращаемого значения зависел от типа входного параметра? Вот тут и нужна перегрузка:

function format(input: string): string;
function format(input: number): string;
function format(input: string | number): string {
  if (typeof input === 'string') return input.trim();
  return input.toFixed(2);
}

const strResult = format(' hello '); // strResult: string
const numResult = format(42);        // numResult: string

Пока разницы нет. Но представьте другой сценарий:

// Без перегрузки — плохо
function parse(input: string | number): string | number {
  if (typeof input === 'string') return parseInt(input, 10);
  return input.toString();
}

const a = parse('123'); // a: string | number — неопределённость!

С перегрузкой:

function parse(input: string): number;
function parse(input: number): string;
function parse(input: string | number): string | number {
  if (typeof input === 'string') return parseInt(input, 10);
  return input.toString();
}

const a = parse('123'); // a: number — точно число!
const b = parse(456);   // b: string — точно строка!

Правила перегрузки

  1. Сигнатуры перегрузки — это объявления без тела. Они определяют, какие комбинации параметров разрешены.
  2. Сигнатура реализации — одна, с телом функции. Она должна быть совместима со всеми сигнатурами перегрузки (обычно более общая).
  3. Сигнатура реализации не видна снаружи — только сигнатуры перегрузки.

Перегрузка с разным количеством параметров

Да, это возможно:

function createUser(name: string): User;
function createUser(name: string, age: number): User;
function createUser(name: string, age?: number): User {
  return age ? { name, age } : { name };
}

const user1 = createUser('Alice');     // ok
const user2 = createUser('Bob', 25);   // ok
// createUser('Charlie', 30, 'extra'); // ошибка — нет такой сигнатуры

Когда использовать перегрузку, а когда union type?

  • Используйте union type, если логика обработки однотипна, а возвращаемый тип не зависит от входного.
  • Используйте перегрузку, когда:
    • Тип возвращаемого значения напрямую зависит от типа входного параметра (как в примере с parse).
    • Разные комбинации параметров ведут к принципиально разным результатам.
    • Вы хотите явно запретить некоторые комбинации, которые допустил бы union type.
  • Используйте дженерики, когда тип возврата связан с типом входа, но логика одинакова (например, function identity<T>(arg: T): T).

Важное предостережение

Не злоупотребляйте перегрузкой. Если у вас больше 3-4 сигнатур — скорее всего, вы делаете что-то не так. Рассмотрите рефакторинг на несколько отдельных функций или использование объектных параметров (options object).

<<>>

Альтернативный подход: объект параметров (options object) вместо перегрузки

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

Пример с вашей функцией format:

interface FormatOptions {
  value: string | number;
  trim?: boolean;
  decimals?: number;
}

function format(options: FormatOptions): string {
  let result: string;
  if (typeof options.value === 'string') {
    result = options.trim ? options.value.trim() : options.value;
  } else {
    result = options.value.toFixed(options.decimals ?? 2);
  }
  return result;
}

// Использование
format({ value: ' hello ' });           // строка, по умолчанию trim = false
format({ value: 42.123, decimals: 1 }); // "42.1"

Преимущества этого подхода:

  • Легко добавлять новые опции без изменения сигнатуры функции.
  • Имена параметров всегда видны в месте вызова (самодокументируемость).
  • IDE лучше подсказывает доступные поля.
  • Меньше шансов перепутать порядок аргументов.

Когда это не подходит:

  • Когда функция должна принимать только один обязательный параметр разных типов (как в примере с parse).
  • Когда вы пишете библиотеку и хотите сохранить обратную совместимость с простым вызовом.

В целом, правило такое: если у вас больше двух перегрузок — подумайте, не упростит ли жизнь объект параметров или дженерики.