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

От автора: большинство функций принимают фиксированный набор аргументов. Но некоторые функции могут принимать переменное количество аргументов, аргументы разных типов и даже могут возвращать разные типы в зависимости от того, как вы вызываете функцию. Для обеспечения такого поведения TypeScript предлагает перегрузку функций.

Посмотрим, как она работает.

1. Сигнатура функции

Рассмотрим функцию, которая возвращает сообщение Hello конкретному человеку:

function greet(person: string): string { return `Hello, ${person}!`;
}

Вышеупомянутая функция принимает 1 аргумент типа string: имя человека. Вызвать функцию довольно просто:

greet('World'); // 'Hello, World!'

Что, если вы хотите сделать функцию greet() более гибкой? Например, сделайте так, чтобы она дополнительно принимала список лиц, которых нужно поприветствовать.

Такая функция могла бы принимать строку или массив строк в качестве аргумента, а также возвращать строку или массив строк.

Как реализовать такую функцию? Есть 2 подхода. Первый подход прост и включает в себя изменение сигнатуры функции напрямую путем обновления типа параметра и возвращаемого значения. Вот как выглядит greet() с обновлением типа параметра и возвращаемого значения:

function greet(person: string | string[]): string | string[] { if (typeof person === 'string') { return `Hello, ${person}!`; } else if (Array.isArray(person)) { return person.map(name => `Hello, ${name}!`); } throw new Error('Unable to greet');
}

Теперь, вы можете вызывать greet() двумя способами:

greet('World'); // 'Hello, World!'
greet(['Jane', 'Joe']); // ['Hello, Jane!', 'Hello, Joe!']

Обновление сигнатуры функции напрямую для поддержки нескольких способов вызова — это обычный и хороший подход. Однако бывают ситуации, когда вы можете захотеть воспользоваться альтернативным подходом и отдельно определить все способы вызова вашей функции. Такой подход называется перегрузкой функций.

2. Перегрузка функции

Второй подход — использовать перегрузку функции. Я рекомендую его, когда сигнатура функции относительно сложна и включает несколько типов.

Внедрение перегрузки на практике требует определения так называемых перегрузки сигнатуры и имплементации сигнатуры.

Перегрузка сигнатуры (оverload signatures) определяет параметры и типы возвращаемых данных функции и не имеет тела. Функция может иметь несколько перегрузок сигнатуры: в соответствии с различными способами вызова функции.

Имплементация сигнатуры (іmplementation signature), с другой стороны, также имеет типы параметров и тип возвращаемого значения, а также тело, реализующее функцию. Реализация сигнатуры может быть только одна. Преобразуем функцию greet(), чтобы использовать перегрузку функции:

// Overload signatures
function greet(person: string): string;
function greet(persons: string[]): string[]; // Implementation signature
function greet(person: unknown): unknown { if (typeof person === 'string') { return `Hello, ${person}!`; } else if (Array.isArray(person)) { return person.map(name => `Hello, ${name}!`); } throw new Error('Unable to greet');
}

Функция greet() имеет 2 перегрузки сигнатуры и одну имплементацию. Каждая перегрузка описывает один из способов вызова функции. В случае функции greet() вы можете вызвать ее двумя способами: со строковым аргументом или с массивом строковых аргументов.

Имплементация сигнатуры function greet(person: unknown): unknown { … } содержит логику работы функции. Теперь, как и раньше, вы можете вызывать greet() с аргументами типа строка или массив строк:

greet('World'); // 'Hello, World!'
greet(['Jane', 'Joe']); // ['Hello, Jane!', 'Hello, Joe!']

2.1 Перегрузка сигнатуры является callable

Хотя имплементация сигнатуры реализует поведение функции, она не может быть вызвана напрямую. Вызывается только перегрузка сигнатуры.

greet('World'); // Overload signature callable
greet(['Jane', 'Joe']); // Overload signature callable const someValue: unknown = 'Unknown';
greet(someValue); // Implementation signature NOT callable

Результат:

No overload matches this call. Overload 1 of 2, '(person: string): string', gave the following error. Argument of type 'unknown' is not assignable to parameter of type 'string'. Overload 2 of 2, '(persons: string[]): string[]', gave the following error. Argument of type 'unknown' is not assignable to parameter of type 'string[]'.

В приведенном выше примере вы не можете вызывать функцию greet() с аргументом типа unknown ( greet(someValue)), даже если имплементация сигнатуры принимает аргумент unknown.

2.2 Имплементация сигнатуры должна быть общей

Имейте в виду, что тип имплементации сигнатуры должен быть достаточно общим, чтобы включать перегрузку сигнатуры. В противном случае TypeScript не примет перегрузку сигнатуры. Например, если вы измените тип возвращаемого значения имплементации сигнатуры с unknown на string:

// Overload signatures
function greet(person: string): string;
function greet(persons: string[]): string[];

Такая перегрузка сигнатуры несовместима с ее имплементацией.

// Implementation signature
function greet(person: unknown): string { // ... throw new Error('Unable to greet');
}

Тогда перегрузка function greet(persons: string[]): string[] помечается как несовместимая с function greet(person: unknown): string.

Тип String возвращаемого значения имплементации недостаточно общий, чтобы быть совместимым с типом string[] перегрузки сигнатуры.

3. Перегрузка методов

В предыдущих примерах перегрузка применялась к обычной функции. Но вы также можете перегрузить и методы! Во время перегрузки метода перегрузка сигнатуры и ее имплементация теперь являются частью класса. Например, давайте реализуем класс Greeter с перегруженным методом greet():

class Greeter { message: string; constructor(message: string) { this.message = message; } // Overload signatures greet(person: string): string; greet(persons: string[]): string[]; // Implementation signature greet(person: unknown): unknown { if (typeof person === 'string') { return `${this.message}, ${person}!`; } else if (Array.isArray(person)) { return person.map(name => `${this.message}, ${name}!`); } throw new Error('Unable to greet'); }
}

Класс Greeter содержит перегрузку метода greet(): 2 перегрузки сигнатуры описывающие то, как метод может быть вызван, и имплементацию, содержащую соответствующую реализацию.

Благодаря перегрузке метода вы можете вызывать hi.greet() двумя способами: используя в качестве аргумента строку или массив строк.

const hi = new Greeter('Hi'); hi.greet('Angela'); // 'Hi, Angela!'
hi.greet(['Pam', 'Jim']); // ['Hi, Pam!', 'Hi, Jim!']

4. Когда использовать перегрузку функций

Перегрузка функций при правильном использовании может значительно повысить удобство использования функций, которые можно вызывать разными способами. Это особенно полезно во время автозаполнения: вы перечисляете все возможные перегрузки в виде отдельных записей в автозаполнении.

Однако бывают ситуации, когда я бы рекомендовал не использовать перегрузку функции, а придерживаться сигнатуры функции. Например, не используйте перегрузку функции для необязательных параметров:

// Not recommended
function myFunc(): string;
function myFunc(param1: string): string;
function myFunc(param1: string, param2: string): string;
function myFunc(...args: string[]): string { // implementation...
}

Достаточно использовать необязательные параметры в сигнатуре функции:

// OK
function myFunc(param1?: string, param2: string): string { // implementation...
}

5. Вывод

Перегрузка функций в TypeScript позволяет определять функции, которые можно вызывать разными способами.
Использование перегрузки функций требует определения сигнатур перегрузки: набора функций с параметрами и возвращаемыми типами, но без тела. Эти сигнатуры указывают, как должна быть вызвана функция.

Кроме того, вы должны написать правильную реализацию функции (имплементацию сигнатуры): параметры и возвращаемые типы, а также тело функции. Обратите внимание, что имплементация сигнатуры не является callable. Помимо обычных функций, методы в классах тоже могут быть перегруженными.

Автор: Dmitri Pavlutin

Источник: dmitripavlutin.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен