narrowing în TypeScript

Narrowing și type guards TypeScript 2.0

TypeScript urmărește verificările din cod (if, switch, return) și restrânge tipul unei variabile în fiecare ramură.

Sintaxă
function f(x: string | number) {
    if (typeof x === "string") {
        // aici x este string
    } else {
        // aici x este number
    }
}

Ce face narrowing

Narrowing (restrângerea tipului) înseamnă că TypeScript citește condițiile din codul tău și știe, în fiecare ramură, ce tip mai poate avea o variabilă. Dacă x este string | number și ai scris if (typeof x === "string"), în interiorul lui if x este string, iar în else este number. Analiza se numește control flow analysis.

Restrâng tipul: typeof, instanceof, in, comparațiile cu ===/!== (inclusiv cu null), verificarea de adevăr (if (x)), Array.isArray, switch pe o proprietate comună (discriminated unions), funcțiile predicat și asserts.

Contează și ieșirile din funcție: după if (!x) return;, restul funcției știe că x nu este gol. La atribuire, tipul se restrânge la valoarea atribuită. Atenție: restrângerea se pierde în interiorul unui callback care rulează mai târziu, dacă variabila este let și se poate schimba între timp.

Exemple

Exemplul 1: Mai multe feluri de restrângere

function descrie(x: string | number | string[] | null): string {
    if (x === null) return "nimic";
    if (typeof x === "number") return `număr ${x.toFixed(2)}`;
    if (Array.isArray(x)) return `listă: ${x.join(" / ")}`;
    return `text de ${x.length} caractere`;     // aici a rămas doar string
}

console.log(descrie(null));
console.log(descrie(3.14159));
console.log(descrie(["Iași", "Cluj"]));
console.log(descrie("TypeScript"));

Rezultat

nimic
număr 3.14
listă: Iași / Cluj
text de 10 caractere

Exemplul 2: Ieșire devreme din funcție

După verificarea de la început, email este sigur string în tot restul funcției.

interface Elev {
    nume: string;
    email?: string;
}

function trimite(e: Elev): string {
    if (!e.email) {
        return `${e.nume} nu are email`;
    }
    const domeniu = e.email.split("@")[1];
    return `Trimis către ${e.email} (domeniul ${domeniu})`;
}

console.log(trimite({ nume: "Ana", email: "ana@scoala.ro" }));
console.log(trimite({ nume: "Mihai" }));

Rezultat

Trimis către ana@scoala.ro (domeniul scoala.ro)
Mihai nu are email

Exemplul 3: Fără verificare, ambele variante rămân posibile

function lungime(x: string | string[] | undefined): number {
    return x.length;
}
console.log(lungime("Ana"));

Eroarea afișată de tsc

exemplu.mts(2,12): error TS18048: 'x' is possibly 'undefined'.

Greșeli frecvente

  • Verifici if (nota) pentru number | undefined. Valoarea 0 este falsă și ramura se sare; compară explicit cu undefined.
  • Verifici tipul într-o funcție separată care returnează boolean. TypeScript nu știe ce înseamnă rezultatul; transformă funcția în predicat (x is Tip).

Comenzi înrudite

  • type guards: Verificări la rulare pe care TypeScript le înțelege și după care știe tipul exact al unei valori.
  • typeof guard: Verificarea typeof x === „string” (sau „number”, „boolean”…) restrânge tipul la primitivul respectiv.
  • discriminated unions: Un union de obiecte cu o proprietate comună de tip literal; verificarea ei arată exact ce variantă ai.
  • predicat de tip (is): O funcție cu rezultatul x is Tip spune compilatorului că, dacă returnează true, x are tipul Tip.
  • union (|): Un tip care acceptă valori din mai multe tipuri: A | B înseamnă fie A, fie B.