Menu

Przeciążanie funkcji w TypeScript: sygnatury przeciążeń

Przeciążenia funkcji w TypeScript pozwalają jednej funkcji mieć kilka sygnatur wywołania, każdą z własnym typem zwracanym. Poznaj wzorzec sygnatur przeciążeń i implementacji, reguły sprawdzane przez kompilator, sytuacje, w których lepszy jest parametr unii, oraz przeciążenia w klasach.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Przeciążanie funkcji w TypeScript polega na napisaniu kilku sygnatur wywołania dla jednej funkcji, a po nich jednej implementacji. Każda sygnatura może łączyć inne typy parametrów z innym typem zwracanym, a wywołujący dostaje ten dokładny.

Bez przeciążeń parse zwracałoby number | number[] przy każdym wywołaniu, a one + 1 byłoby błędem, dopóki sam nie zawęzisz wyniku.

Sygnatury przeciążeń i implementacja

Przeciążona funkcja ma dwie części:

  1. Sygnatury przeciążeń: deklaracje bez ciała, po jednej na każdy obsługiwany kształt wywołania. Tylko tych sygnatur mogą używać wywołujący.
  2. Sygnatura implementacji: ostatnia deklaracja, z ciałem. Jej parametry muszą przyjmować wszystko, co przyjmują przeciążenia, a jej typ zwracany musi obejmować typ zwracany każdego przeciążenia. Z zewnątrz jest niewidoczna.

Typy istnieją tylko w czasie kompilacji, więc w czasie wykonania jest jedna funkcja JavaScript. Implementacja musi sprawdzić swoje argumenty (typeof, Array.isArray, arguments.length...), aby zdecydować, co zrobić. Kompilator sprawdza, czy przeciążenia i implementacja się zgadzają:

function format(value: string): string;
function format(value: number): number {
  return value;
}
// error TS2394: This overload signature is not compatible with its implementation signature.

Rozwiązanie to poszerzenie implementacji: function format(value: string | number): string | number.

Sygnatury implementacji nie da się wywołać

To reguła, która najbardziej zaskakuje. Wywołanie musi samodzielnie pasować do jednej z sygnatur przeciążeń; TypeScript ich nie łączy.

Kompilator wypisuje:

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[]'.

Implementacja przyjmuje string | string[], ale wywołujący jej nie widzą. Dodaj trzecie przeciążenie, które przyjmuje i zwraca unię, a wywołanie się skompiluje i wypisze [ 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);
}

Różna liczba parametrów

Przeciążenia opisują też wywołania z różną liczbą argumentów. Tutaj datę można zbudować ze znacznika czasu albo z roku, miesiąca i dnia, ale nie z dwóch liczb:

Jedna sygnatura z dwoma opcjonalnymi parametrami przyjęłaby makeDate(2024, 3) i po cichu zbudowała złą datę. Przeciążenia zamieniają to w błąd kompilacji (TS2575).

Kolejność ma znaczenie

TypeScript próbuje przeciążeń od góry do dołu i wybiera pierwsze pasujące. Najbardziej szczegółowe sygnatury umieszczaj na początku. Szerokie przeciążenie na początku listy przechwytuje wywołania przeznaczone dla kolejnych:

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"

Zamień miejscami dwie pierwsze sygnatury, a describe("hi") dostanie typ "text".

Przeciążenia czy parametr unii?

Przeciążenia są warte dodatkowych linii, gdy typ zwracany zależy od typów argumentów. Gdy nie zależy, jedna sygnatura z parametrem unii jest krótsza, czytelniejsza i przyjmuje argumenty typu unii, które przeciążenia by odrzuciły.

UżyjKiedy
Parametru uniiTen sam typ zwracany dla każdego wejścia
Parametrów opcjonalnychKształty wywołań różnią się tylko końcowymi argumentami, które można swobodnie pominąć
PrzeciążeńTyp zwracany zmienia się zależnie od argumentów albo niektóre kombinacje argumentów trzeba odrzucić
GenerykaTyp zwracany powstaje z typu argumentu, jak w identity<T>(x: T): T

Generyk z conditional type potrafi wyrazić niektóre zestawy przeciążeń jedną sygnaturą, ale przy dwóch lub trzech przypadkach przeciążenia są zwykle czytelniejsze.

Przeciążone metody i konstruktory

Metody używają tego samego wzorca wewnątrz klasy: sygnatury przeciążeń, a potem metoda z ciałem. Konstruktory można przeciążać tak samo.

Interfejsy i typy obiektowe też mogą deklarować przeciążenia, jako kilka sygnatur wywołania albo kilka sygnatur metod o tej samej nazwie. Wiele wbudowanych funkcji jest zadeklarowanych właśnie tak: najedź kursorem na reduce na tablicy w edytorze, a zobaczysz "+2 overloads".

Najczęściej zadawane pytania

Czy TypeScript obsługuje przeciążanie funkcji?

Tak, na poziomie typów. Piszesz kilka sygnatur przeciążeń (deklaracji bez ciała), a po nich jedną implementację. Wywołujący widzą tylko sygnatury przeciążeń. W czasie wykonania nadal istnieje tylko jedna funkcja JavaScript, więc implementacja sama sprawdza argumenty i obsługuje każdy przypadek.

Co oznacza "No overload matches this call"?

Błąd TS2769: argumenty nie pasują do żadnej sygnatury przeciążenia. Sygnatura implementacji się nie liczy, więc wywołanie z argumentem typu unii, takim jak string | string[], kończy się błędem, nawet gdy implementacja go przyjmuje. Dodaj przeciążenie przyjmujące unię albo zastąp przeciążenia jedną sygnaturą.

Kiedy używać przeciążeń zamiast typu unii?

Używaj przeciążeń, gdy typ zwracany zależy od typów przekazanych argumentów, na przykład string na wejściu daje number na wyjściu, a string[] na wejściu daje number[]. Gdy typ zwracany jest taki sam dla każdego wejścia, jedna sygnatura z parametrem unii jest prostsza i przyjmuje też argumenty typu unii.

Czy funkcje strzałkowe można przeciążać w TypeScript?

Nie składnią deklaracji przeciążeń, która działa tylko dla deklaracji function i metod. Możesz nadać zmiennej przeciążony typ z kilkoma sygnaturami wywołania, type Parse = { (s: string): number; (s: string[]): number[] }, ale przypisanie do niej funkcji strzałkowej zwykle wymaga asercji typu, więc deklaracja function jest czystszym wyborem.

Dlaczego moja sygnatura przeciążenia nie jest zgodna z sygnaturą implementacji?

Błąd TS2394 oznacza, że któreś przeciążenie przyjmuje lub zwraca coś, czego implementacja nie obsługuje. Parametry implementacji muszą przyjmować parametry każdego przeciążenia, a jej typ zwracany musi być zgodny z typem zwracanym każdego przeciążenia. Pomaga poszerzenie implementacji (często do unii).

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ