programowanieFrontend/Fullstack programista

Jak działa typ ReturnType<T> w TypeScript, czym różni się od ręcznego określania typu wartości zwracanej przez funkcję i jakie ryzyka/korzyści wiążą się z jego użyciem?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Historia pytania

Wraz z rozwojem TypeScript pojawiła się potrzeba automatycznego wyodrębniania typu wartości zwracanej z funkcji, zwłaszcza w dużych projektach z wieloma powiązanymi ze sobą funkcjami. W tym celu wprowadzono typ pomocniczy ReturnType<T>, który został wprowadzony w standardowej bibliotece od wersji TypeScript 2.8.

Problem

W dużych i złożonych projektach może być trudno utrzymać aktualność typów — jeśli ręcznie określa się typ wartości zwracanej każdej funkcji i jednocześnie wprowadza zmiany w sygnaturach, łatwo może pojawić się niespójność, gdy sygnatury i implementacje nie pasują do siebie. Dodatkowo, jeśli funkcja zwraca złożoną strukturę, nie zawsze łatwo jest ręcznie zsynchronizować opis zwracanego typu we wszystkich miejscach użycia.

Rozwiązanie

Typ pomocniczy ReturnType<T> automatycznie wyodrębnia typ zwracany przez funkcje i może być używany do typizacji wyniku wywołania funkcji w dowolnym miejscu kodu. To zmniejsza ilość ręcznego wsparcia infrastruktury typów i minimalizuje błędy związane z niespójnością opisanego i rzeczywistego typu wartości zwracanej.

Przykład kodu:

function createUser(name: string, age: number) { return { name, age, created: new Date() }; } type User = ReturnType<typeof createUser>; // User: { name: string; age: number; created: Date; }

Kluczowe cechy:

  • Automatycznie wyodrębnia typ wartości zwracanej funkcji (w tym generiki), eliminując duplikację kodu.
  • Zmniejsza ryzyko błędu w przypadku zmian sygnatury funkcji lub struktury wartości zwracanej.
  • Nie działa z przeciążeniami funkcji — wyodrębnia tylko ogólny (szeroki) typ zwracany.

Pytania podchwytliwe.

Czy można używać ReturnType z metodami klas?

Tak, ale ważne jest, aby pamiętać o kontekście: jeśli metoda jest właściwością obiektu z funkcją, należy użyć ReturnType<obj['metoda']>.

Przykład kodu:

class MyClass { foo(x: number) { return x * 2; } } type FooReturn = ReturnType<MyClass['foo']>; // Błąd typu! // Należy tak: type FooReturn = ReturnType<(x: number) => number>; // number // Lub wyodrębnić metodę jako funkcję: const obj = new MyClass(); type FooReturn2 = ReturnType<typeof obj.foo>;

Co zwróci ReturnType dla funkcji z void/never?

Dla funkcji o zadeklarowanym typie void, ReturnType zwróci void. Dla never — never.

Przykład kodu:

function doNothing(): void {} type Result = ReturnType<typeof doNothing>; // void

Czy ReturnType działa z przeciążeniami funkcji?

Nie, ReturnType wyodrębnia zwracany typ samej "implementacji", a nie wszystkich przeciżeń. Jeśli jest kilka przeciżeń — brany jest opisany typ implementacji.

Przykład kodu:

function func(x: number): number; function func(x: string): string; function func(x: any): any { return x } type RT = ReturnType<typeof func>; // any

Typowe błędy i antywzorce

  • Użycie ReturnType z przeciążonymi funkcjami prowadzi do nieprzewidywanych typów.
  • Zapomnienie, że ReturnType nie oblicza return-typu wartości Promise — potrzebny jest Awaited lub ręczna praca.
  • Całkowite poleganie na ReturnType podczas zmiany logiki funkcji bez aktualizacji innych zależnych części kodu.

Przykład z życia

Negatywny przypadek

W projekcie zadeklarowano ręczny typ dla zwracanego obiektu funkcji. Funkcja się zmienia — typ nie jest aktualizowany, wywołania padają w czasie wykonywania.

Zalety:

  • Typy są łatwe do odczytania, można je regulować ręcznie.

Wady:

  • Szybko się dezaktualizują, powstaje "dryf" między typami a faktycznym API.

Pozytywny przypadek

Przechodzą na ReturnType wszędzie tam, gdzie używają wartości zwracanej przez funkcję. W przypadku zmian typ jest zawsze aktualny.

Zalety:

  • Minimalne duplikowanie, typizowane dopasowanie do faktycznej implementacji.

Wady:

  • Mogą występować trudności w zrozumieniu magii typów przez nowicjuszy.