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:
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
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:
Wady:
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:
Wady: