programowanieFrontend developer

Jak zaimplementować i typować funkcję, która zwraca typ unii lub krotkę w zależności od parametrów? Jak poprawnie opisać wartości zwracane, aby nie stracić ścisłej typizacji podczas dalszych operacji i używania destrukturyzacji?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Historia pytania:

W niektórych zadaniach wymagane jest, aby funkcja mogła zwracać różne struktury danych — na przykład tablicę o stałej długości (krotka) lub kilka wariantów wyniku (typ unii). W dynamicznym JavaScript podejście to jest szeroko stosowane w wzorcach „wynik lub błąd” (np. [err, value]), natomiast w TypeScript wymaga wyraźnej typizacji, aby późniejszy kod prawidłowo rozumiał strukturę wyniku.

Problem:

Bez wyraźnego zdefiniowania zwracanego typu TypeScript prowadzi wynik do zbyt „szerokiej” (tj. nie wąskiej) formy — tablicy lub unii, co pozbawia programistę zalet autouzupełniania i bezpieczeństwa typów. Jeśli zdestrukturować wynik funkcji, istnieje ryzyko utraty dokładnego opisu typów elementów.

Rozwiązanie:

Należy użyć typów warunkowych i przeciążeń (overload) funkcji, a dla krotek — wyraźnie określać typ zwracanej tablicy oraz używać as const (jeśli zwracamy stałą krotkę). Dla wyników unii można łączyć generiki z ograniczaniem typów, aby na zewnątrz kodu można było poprawnie rozróżniać typ zwracanego wyniku.

Przykład kodu:

type Result<T> = [null, T] | [Error, null]; function parseNumber(str: string): Result<number> { const n = Number(str); return isNaN(n) ? [new Error('Nieprawidłowy numer'), null] : [null, n]; } const [err, value] = parseNumber('123'); if (err) console.error(err.message); else console.log(value!.toFixed(2)); // Z krotką i as const dla auto-rozmytych struktur: function getStatus(flag: boolean): [string, number] | string { return flag ? ['ok', 200] as const : 'error'; } const r = getStatus(true); if (Array.isArray(r)) { // r: readonly [string, number] }

Kluczowe cechy:

  • Używaj przeciążeń (overloads) dla funkcji, które zwracają różne typy w zależności od parametrów.
  • Dla krotek zawsze używaj [T, U] lub as const, aby zachować dokładną długość i typ elementów po destrukturyzacji.
  • Typy unii zwracanych wartości wymagają strażników typów do wąskiego określenia typu podczas dalszej pracy.

Pytania z podchwytliwymi odpowiedziami.

Czy typ elementu tablicy po destrukturyzacji unii zwróconej krotki zawsze będzie konkretny?

Nie, jeśli funkcja zwraca unię krotek, po destrukturyzacji elementy będą reprezentować połączenie typów wszystkich możliwych wariacji.

type R = [number, null] | [null, string]; const [a, b]: R = [1, null]; // a: number | null // b: null | string

Czy można przy pomocy as const całkowicie "zafiksować" typ krotki bez dodatkowego opisu typu funkcji?

Nie, as const ustala wartości, ale jeśli funkcja jest zadeklarowana bez zwracanego typu, TypeScript może wnioskować typ szerzej niż to potrzebne. Lepiej wyraźnie określić typ zwracany.

function foo() { return [1, 'ok'] as const; } // foo(): readonly [1, "ok"]

Czy przeciążenia z różnymi zwracanymi typami gwarantują określenie typu wyniku po destrukturyzacji?

Przeciążenia pomagają kompilatorowi, ale jeśli parametr wejściowy jest nieznany, wynik będzie unią wszystkich opcji. Aby dokładnie zawęzić typ, musisz użyć strażników typów w przetworniku wyniku.

function bar(x: number): string; function bar(x: boolean): number; function bar(x: any): any { return typeof x === 'number' ? 'str' : 123; } const r = bar(Math.random() > 0.5 ? true : 1); // r: number | string

Typowe błędy i antywzorce

  • Opisanie funkcji jako zwracającej any[] lub (T | U)[], co prowadzi do "rozmywania" specyfiki krotki.
  • Próba użycia destrukturyzacji bez ograniczania typów i sprawdzania poprawności typu elementów.
  • Brak bezpośredniego opisu struktury zwracanej wartości — komplikacja wsparcia i autouzupełniania.

Przykład z życia

Negatywny przypadek

Funkcja zwraca albo tablicę, albo błąd, ale nie opisuje tego wyraźnie w typie. W kodzie wywołującym oczekiwane jest, że wynik to liczba, a odwołanie do result[1].toFixed(2) powoduje błąd w czasie wykonywania.

Zalety:

  • Prosty do napisania kod bez typizacji.

Wady:

  • Błąd tylko w czasie wykonywania.
  • Utrata ścisłej typizacji, więcej błędów.

Pozytywny przypadek

Funkcja zwraca ściśle typizowaną krotkę z typem Result<T>, przetwarzanie wyniku opiera się na destrukturyzacji i wyraźnej kontroli runtime pierwszego elementu (error/null). Kompilator gwarantuje, że można odwoływać się do wyniku tylko po pomyślnym zawężeniu typu.

Zalety:

  • Przewidywalność i klarowność kodu.
  • Automatyczne autouzupełnianie dla krotek.

Wady:

  • Konieczność ręcznego opisywania typów.
  • Wzrost liczby strażników typów i kontroli.