TypeScript: Перегрузка функций (Function Overloads)
В этом уроке мы научимся использовать перегрузки функций.
Использование перегрузки функций
Перегрузка функций — это возможность определить несколько версий одной функции, каждая из которых принимает свой набор параметров. Разберем на примере:
function concat(a: number, b: number): string;
function concat(a: string, b: string): string;
function concat(a: unknown, b: unknown): string {
if (typeof a === 'number' && typeof b === 'number') {
return `${a.toFixed()}${b.toFixed()}`;
}
return `${a}${b}`;
}
concat('one', 'two'); // onetwo
concat(3, 5.34); // 35
concat(1.33, 10); // 110
Здесь определяется одна функция concat()
. У нее две версии, которые выполняют конкатенацию, но делают это по-разному:
- Первая версия — принимает на вход два числа. У чисел сначала отбрасывается дробная часть, потом они конкатенируются
- Вторая версия — принимает на вход две строки. Строки конкатенируются сразу
Реализация поведения для обеих версий делается в третьей функции с тем же именем. При этом описание параметров должно подходить под каждую версию функции. В примере выше типы параметров определены как unknown
. Это дает возможность вызывать функцию как со строками, так и с числами.
То, по какой ветке идти, определяется с помощью проверки типов. В примере выше достаточно проверить тип только первого параметра, так как второй в таком случае точно будет строкой. Это обеспечивает система типов и компилятор.
Для перегрузки необязательно использовать объявление функций. То же самое можно сделать с помощью стрелочной функции:
const concat: {
(a: number, b: number): string;
(a: string, b: string): string;
} = (a: unknown, b: unknown) => {
if (typeof a === 'number' && typeof b === 'number') {
return `${a.toFixed()}${b.toFixed()}`;
}
return `${a}${b}`;
}
// с использованием псевдонимов
type Overloaded = {
(a: number, b: number): string;
(a: string, b: string): string;
}
const concat: Overloaded = (a, b) => {...}
В этом случае не обязательно явно указывать типы параметров внутри функции. Это делается в объявлении функции.
Перегрузка функций не ограничивается двумя версиями. Их может быть сколько угодно. Главное, что в конце всегда описывается функция, которая является общей по параметрам для всех вариантов и внутри которой описывается вся логика для каждого варианта.
Опишем функцию add()
, которая может принимать на вход два или три числа и возвращает их сумму:
function add(a: number, b: number, c: number): number;
function add(a: number, b: number): number;
function add(a: string, b: string): string;
// Сигнатура подходит под все примеры выше
function add(a: unknown, b: unknown, c?: number): unknown {
// тут вся логика
if (c === undefined) {
// ...
}
}
В статических языках перегрузка функций используется достаточно часто, но в большинстве из них она устроена не как в TypeScript. В этих языках создается несколько разных функций, которые с точки зрения программиста имеют одно имя. Поэтому там не нужна общая функция. Логика каждого варианта описывается внутри. Это избавляет код от необходимости реализовывать условную логику.
// Пример на Kotlin
fun main() {
fun newYearCongratulate (name:String):String {
return "Hi ${name}! Happy New Year!"
}
fun newYearCongratulate (year: Number, name:String):String {
return "Hi ${name}! Happy New Year ${year}!"
}
println(newYearCongratulate("John")) // Hi John! Happy New Year!
println(newYearCongratulate(2023, "Elon")) // Hi Elon! Happy New Year 2023!
}
Зачем в TypeScript такая реализация и какие проблемы она решает? Это как и многое другое в TypeScript — попытка учесть все варианты написания кода на JavaScript и покрыть их типами для написания типобезопасного кода.
В JavaScript нередко создают функции, которые принимают на вход разные типы данных в разных вариациях. Перегрузка функций позволяет описать подобные функции в TypeScript, иначе пришлось бы использовать any
и следить за типами самостоятельно.
Технически после транспиляции в JavaScript остается одна функция — та, что содержит тело.
Перегрузка функций в TypeScript — это механизм, который стоит использовать, когда нет другого выбора. В большинстве случаев вместо перегрузки используются объединения или дженерики, о которых мы поговорим позже.
Перегрузка нужна, когда между параметрами есть зависимость, например, если оба параметра — строки, либо оба параметра — числа.
Задание
Реализуйте функцию newYearCongratulate()
, которая аналогична примеру на Kotlin из теории:
newYearCongratulate('John'); // Hi John! Happy New Year!
newYearCongratulate(2023, 'Mila'); // Hi Mila! Happy New Year 2023!
Упражнение не проходит проверку — что делать? 😶
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя 🤔
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно 🙄
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.