discriminated unions în TypeScript

Narrowing și type guards TypeScript 2.0

Un union de obiecte cu o proprietate comună de tip literal; verificarea ei arată exact ce variantă ai.

Sintaxă
type Forma =
    | { tip: "cerc"; raza: number }
    | { tip: "patrat"; latura: number };

switch (forma.tip) { case "cerc": ... }

Ce face discriminated unions

Un discriminated union (union etichetat) este un union de tipuri obiect care au toate o proprietate cu același nume, dar cu o valoare literală diferită: tip: "cerc", tip: "patrat". Proprietatea comună se numește discriminant. Când verifici valoarea ei, cu if sau switch, TypeScript știe exact care variantă este și ce alte proprietăți are.

Este cel mai sigur mod de a modela date care pot avea mai multe forme: răspunsuri de la server (succes/eroare), evenimente, stări ale unei comenzi, acțiuni în Redux. Fiecare variantă are doar proprietățile ei, deci nu poți citi din greșeală raza la un pătrat.

Împreună cu never în ramura default, obții verificare exhaustivă: dacă adaugi o variantă nouă și uiți s-o tratezi, tsc dă eroare.

Exemple

Exemplul 1: Răspuns de succes sau de eroare

type Raspuns<T> =
    | { stare: "ok"; date: T }
    | { stare: "eroare"; cod: number; mesaj: string }
    | { stare: "incarcare" };

function afiseaza(r: Raspuns<string[]>): string {
    switch (r.stare) {
        case "ok":
            return `${r.date.length} elevi: ${r.date.join(", ")}`;
        case "eroare":
            return `Eroare ${r.cod}: ${r.mesaj}`;
        case "incarcare":
            return "Se încarcă…";
    }
}

console.log(afiseaza({ stare: "incarcare" }));
console.log(afiseaza({ stare: "ok", date: ["Ana", "Mihai"] }));
console.log(afiseaza({ stare: "eroare", cod: 404, mesaj: "Clasa nu există" }));

Rezultat

Se încarcă…
2 elevi: Ana, Mihai
Eroare 404: Clasa nu există

Exemplul 2: Evenimente într-un magazin online

type Eveniment =
    | { tip: "adaugare"; produs: string; cantitate: number }
    | { tip: "stergere"; produs: string }
    | { tip: "golire" };

function aplica(cos: Map<string, number>, e: Eveniment): void {
    if (e.tip === "adaugare") cos.set(e.produs, (cos.get(e.produs) ?? 0) + e.cantitate);
    else if (e.tip === "stergere") cos.delete(e.produs);
    else cos.clear();
}

const cos = new Map<string, number>();
const evenimente: Eveniment[] = [
    { tip: "adaugare", produs: "mere", cantitate: 2 },
    { tip: "adaugare", produs: "pâine", cantitate: 1 },
    { tip: "adaugare", produs: "mere", cantitate: 3 },
    { tip: "stergere", produs: "pâine" },
];
for (const e of evenimente) aplica(cos, e);
console.log(cos);

Rezultat

Map(1) { 'mere' => 5 }

Exemplul 3: Proprietate a altei variante

type Forma = { tip: "cerc"; raza: number } | { tip: "patrat"; latura: number };
function arie(f: Forma): number {
    if (f.tip === "cerc") return f.latura ** 2;
    return f.latura ** 2;
}
console.log(arie({ tip: "patrat", latura: 2 }));

Eroarea afișată de tsc

exemplu.mts(3,36): error TS2339: Property 'latura' does not exist on type '{ tip: "cerc"; raza: number; }'.

Greșeli frecvente

  • Faci discriminantul de tip string în loc de literal (tip: string). TypeScript nu mai poate deosebi variantele.
  • Destructurezi obiectul înainte de verificare (const { tip, raza } = f). raza nu există pe toate variantele; verifică întâi f.tip.

Comenzi înrudite

  • union (|): Un tip care acceptă valori din mai multe tipuri: A | B înseamnă fie A, fie B.
  • never: Tipul fără nicio valoare: pentru funcții care nu se termină normal și pentru cazurile imposibile din cod.
  • narrowing: TypeScript urmărește verificările din cod (if, switch, return) și restrânge tipul unei variabile în fiecare ramură.
  • tipuri literale: Un tip care acceptă o singură valoare exactă, de exemplu „Iași” sau 3; combinate în union dau liste de valori permise.
  • in guard: „cheie” in x verifică dacă obiectul are proprietatea și restrânge union-ul la variantele care o au.