Перегрузка функций в TypeScript: как правильно объявить и использовать?
Что такое перегрузка функций в 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 — точно строка!
Правила перегрузки
- Сигнатуры перегрузки — это объявления без тела. Они определяют, какие комбинации параметров разрешены.
- Сигнатура реализации — одна, с телом функции. Она должна быть совместима со всеми сигнатурами перегрузки (обычно более общая).
- Сигнатура реализации не видна снаружи — только сигнатуры перегрузки.
Перегрузка с разным количеством параметров
Да, это возможно:
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). - Когда вы пишете библиотеку и хотите сохранить обратную совместимость с простым вызовом.
В целом, правило такое: если у вас больше двух перегрузок — подумайте, не упростит ли жизнь объект параметров или дженерики.
Ответов пока нет.
Войдите чтобы оставить ответ