Перегрузка функций в TypeScript означает, что для одной функции пишется несколько сигнатур вызова, а за ними одна реализация. Каждая сигнатура может связывать свои типы параметров со своим возвращаемым типом, и вызывающий код получает точный тип.
Без перегрузок parse возвращала бы number | number[] для любого вызова, и one + 1 оставалось бы ошибкой, пока вы сами не сузили бы результат.
Сигнатуры перегрузки и реализация
У перегруженной функции две части:
- Сигнатуры перегрузки: объявления без тела, по одному на каждую поддерживаемую форму вызова. Только эти сигнатуры доступны вызывающему коду.
- Сигнатура реализации: последнее объявление, с телом. Её параметры должны принимать всё, что принимают перегрузки, а её возвращаемый тип должен покрывать возвращаемый тип каждой перегрузки. Снаружи она не видна.
Типы существуют только во время компиляции, поэтому во время выполнения функция JavaScript одна. Реализация должна изучить свои аргументы (typeof, Array.isArray, arguments.length...), чтобы решить, что делать. Компилятор проверяет, что перегрузки и реализация согласованы:
function format(value: string): string;
function format(value: number): number {
return value;
}
// error TS2394: This overload signature is not compatible with its implementation signature.
Исправление: расширить реализацию, function format(value: string | number): string | number.
Сигнатуру реализации нельзя вызвать
Это правило удивляет больше всего. Вызов должен сам по себе подходить к одной из сигнатур перегрузки; TypeScript их не объединяет.
Компилятор выводит:
index.ts(12,19): error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'string | string[]' is not assignable to parameter of type 'string[]'.
Type 'string' is not assignable to type 'string[]'.
Реализация принимает string | string[], но вызывающий код её не видит. Добавьте третью перегрузку, которая принимает объединение и возвращает объединение, и вызов скомпилируется и напечатает [ 1, 2 ]:
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]): number | number[];
function parse(input: string | string[]): number | number[] {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
Разное количество параметров
Перегрузки описывают и вызовы с разным числом аргументов. Здесь дату можно построить из метки времени или из года, месяца и дня, но не из двух чисел:
Одна сигнатура с двумя опциональными параметрами приняла бы makeDate(2024, 3) и молча построила бы неверную дату. Перегрузки превращают это в ошибку компиляции (TS2575).
Порядок имеет значение
TypeScript перебирает перегрузки сверху вниз и выбирает первую подходящую. Ставьте самые конкретные сигнатуры первыми. Широкая перегрузка в начале списка перехватывает вызовы, предназначенные для следующих за ней:
function describe(value: unknown): string; // matches everything
function describe(value: string): "text"; // never chosen
function describe(value: unknown): string {
return typeof value === "string" ? "text" : "other";
}
const d = describe("hi"); // d: string, not "text"
Поменяйте местами первые две сигнатуры, и describe("hi") получит тип "text".
Перегрузки или параметр-объединение?
Перегрузки оправдывают лишние строки, когда возвращаемый тип зависит от типов аргументов. Когда не зависит, одна сигнатура с параметром-объединением короче, проще читается и принимает аргументы-объединения, которые перегрузки отвергли бы.
| Что использовать | Когда |
|---|---|
| Параметр-объединение | Возвращаемый тип одинаков для любого входа |
| Опциональные параметры | Формы вызова различаются только последними аргументами, которые можно свободно пропускать |
| Перегрузки | Возвращаемый тип меняется вместе с аргументами или некоторые комбинации аргументов нужно отвергать |
| Дженерик | Возвращаемый тип строится из типа аргумента, например identity<T>(x: T): T |
Дженерик с условным типом может выразить некоторые наборы перегрузок одной сигнатурой, но для двух-трёх случаев перегрузки обычно читаются легче.
Перегруженные методы и конструкторы
Методы внутри класса используют ту же схему: сигнатуры перегрузки, затем метод с телом. Так же можно перегружать конструкторы.
Интерфейсы и объектные типы тоже могут объявлять перегрузки: несколько сигнатур вызова или несколько сигнатур метода с одним именем. Так объявлены многие встроенные функции: наведите курсор на reduce у массива в редакторе, и он покажет «+2 overloads».
Часто задаваемые вопросы
Поддерживает ли TypeScript перегрузку функций?
Да, на уровне типов. Вы пишете несколько сигнатур перегрузки (объявлений без тела), за которыми следует одна реализация. Вызывающий код видит только сигнатуры перегрузки. Во время выполнения функция JavaScript по-прежнему одна, поэтому реализация сама проверяет аргументы и обрабатывает каждый случай.
Что означает «No overload matches this call»?
Ошибка TS2769: аргументы не подходят ни к одной сигнатуре перегрузки. Сигнатура реализации не учитывается, поэтому вызов с аргументом-объединением вроде string | string[] падает, даже если реализация его принимает. Добавьте перегрузку, которая принимает объединение, или замените перегрузки одной сигнатурой.
Когда использовать перегрузки вместо объединения типов?
Используйте перегрузки, когда возвращаемый тип зависит от того, какие типы аргументов переданы: например, string на входе даёт number на выходе, а string[] на входе даёт number[]. Когда возвращаемый тип одинаков для любого входа, одна сигнатура с параметром-объединением проще и к тому же принимает аргументы-объединения.
Можно ли перегружать стрелочные функции в TypeScript?
Не синтаксисом объявления перегрузок: он работает только для объявлений function и методов. Переменной можно дать перегруженный тип с несколькими сигнатурами вызова, type Parse = { (s: string): number; (s: string[]): number[] }, но присвоение ей стрелочной функции обычно требует утверждения типа, поэтому объявление function чище.
Почему сигнатура перегрузки несовместима с сигнатурой реализации?
Ошибка TS2394 означает, что какая-то перегрузка принимает или возвращает то, чего не принимает или не возвращает реализация. Параметры реализации должны принимать параметры каждой перегрузки, а её возвращаемый тип должен быть совместим с возвращаемым типом каждой перегрузки. Помогает расширение реализации (часто до объединения).