Перегрузка функций в TypeScript: как правильно объявить и использовать?
Почему возникает ошибка и как её исправить
Ошибка возникает из-за несовместимости сигнатур перегрузки с сигнатурой реализации. В TypeScript сигнатура реализации должна быть совместима со всеми объявленными перегрузками. В вашем случае:
- Первая перегрузка:
(a: string, b: number) => string - Вторая перегрузка:
(a: string, b: string) => string - Третья перегрузка:
(a: number) => string - Реализация:
(a: string | number, b?: string | number) => string
Проблема в том, что реализация допускает вызов format("hello", undefined) (только один строковый аргумент), а ни одна из перегрузок этого не разрешает. TypeScript проверяет, что реализация может принять любой вызов, который разрешён перегрузками, но не наоборот. Реализация должна быть более общей, чем все перегрузки вместе взятые, но при этом не добавлять новых допустимых комбинаций.
Правильное решение
Вот исправленный код:
function format(a: string, b: number): string;
function format(a: string, b: string): string;
function format(a: number): string;
function format(a: string | number, b?: string | number): string {
if (typeof a === 'number') {
return `ID: ${a}`;
}
if (typeof b === 'number') {
return a.repeat(b);
}
if (typeof b === 'string') {
return `${a} - ${b}`;
}
// Этот случай никогда не выполнится при корректном вызове,
// но нужен для TypeScript, чтобы реализация покрывала все случаи
throw new Error('Invalid arguments');
}
Ключевое изменение: мы добавили throw в конце. Это гарантирует, что реализация возвращает string для любого возможного вызова, даже если он не соответствует ни одной перегрузке (хотя такие вызовы будут ошибкой на этапе компиляции).
Альтернативный подход: объединение типов через union
Перегрузка функций — мощный, но не единственный способ. Для простых случаев можно использовать объединение типов параметров:
type FormatParams =
| { a: string; b: number }
| { a: string; b: string }
| { a: number };
function format(params: FormatParams): string {
if ('b' in params) {
if (typeof params.b === 'number') {
return params.a.repeat(params.b);
}
return `${params.a} - ${params.b}`;
}
return `ID: ${params.a}`;
}
// Использование:
format({ a: "hello", b: 3 }); // "hellohellohello"
format({ a: "hello", b: "world" }); // "hello - world"
format({ a: 42 }); // "ID: 42"
Этот подход более гибкий и легче поддерживается, особенно если количество вариантов растёт.
Ещё один вариант: кортежи и discriminated unions
Можно использовать кортежи с литеральными типами для диспетчеризации:
type FormatArgs =
| ['repeat', string, number]
| ['join', string, string]
| ['id', number];
function format(...args: FormatArgs): string {
const [action, ...rest] = args;
switch (action) {
case 'repeat':
return rest[0].repeat(rest[1]);
case 'join':
return `${rest[0]} - ${rest[1]}`;
case 'id':
return `ID: ${rest[0]}`;
}
}
// Использование:
format('repeat', "hello", 3); // "hellohellohello"
format('join', "hello", "world"); // "hello - world"
format('id', 42); // "ID: 42"
Этот паттерн часто используется в Redux-подобных архитектурах и делает код самодокументируемым.
Важные замечания
- Перегрузки в TypeScript — это только типы. Во время выполнения остаётся только одна реализация.
- Порядок перегрузок важен: TypeScript проверяет их сверху вниз и использует первую подходящую.
- Сигнатура реализации не видна снаружи — только объявленные перегрузки.
- Для сложной логики лучше использовать discriminated unions или объекты с тегом, чем множественные перегрузки.
Использование дженериков и условных типов
Ещё один элегантный способ — применить дженерики в сочетании с условными типами. Это позволяет TypeScript автоматически выводить тип возвращаемого значения:
type FormatResult<T extends any[]> =
T extends [string, number] ? string :
T extends [string, string] ? string :
T extends [number] ? string :
never;
function format<T extends [string, number] | [string, string] | [number]>(
...args: T
): FormatResult<T> {
const [a, b] = args;
if (typeof a === 'number') {
return `ID: ${a}` as any;
}
if (typeof b === 'number') {
return a.repeat(b) as any;
}
return `${a} - ${b}` as any;
}
Плюсы:
- Автоматический вывод типа возвращаемого значения.
- Нет необходимости писать несколько сигнатур вручную.
- Легко расширять новыми вариантами.
Минусы:
- Сложнее для понимания начинающими.
- Требует приведения типов (
as any) в реализации.
Этот подход особенно полезен, когда у вас много вариантов вызова и вы хотите сохранить строгую типизацию без дублирования кода. Для простых случаев, как в вашем примере, лучше использовать классическую перегрузку или discriminated union.