Rozlišená sjednocení

Na konci minulé lekce jste lektvary od zbraní rozlišovali podle jejich vlastností, pomocí "leceni" in predmet. Funguje to, ale je to křehké: TypeScript si nevšimne překlepu jako "lecen" in predmet, a když jednou vlastnost leceni dostane i jiný druh předmětu, kontrola potichu přestane fungovat.

Obvyklý způsob, jak v TypeScriptu popsat věci několika různých druhů, je dát každému objektu vlastnost, která říká, co je zač. V tomto kurzu se jí bude říkat druh (v anglickém kódu se obvykle jmenuje kind) a v každém typu sjednocení má jiný literálový typ:

type Lektvar = { druh: "lektvar"; nazev: string; leceni: number };
type Zbran = { druh: "zbran"; nazev: string; poskozeni: number };
type Predmet = Lektvar | Zbran;

const inventar: Predmet[] = [
    { druh: "lektvar", nazev: "Léčivý lektvar", leceni: 20 },
    { druh: "zbran", nazev: "Stříbrný meč", poskozeni: 12 },
];

Takovému sjednocení se říká rozlišené sjednocení (anglicky discriminated union): všechny jeho typy mají společnou vlastnost druh a podle její hodnoty se od sebe rozliší. Té vlastnosti se také říká značka (anglicky tag). Kontrola značky typ zúží:

function popisPredmetu(predmet: Predmet): string {
    if (predmet.druh === "lektvar") {
        return `${predmet.nazev} (léčí ${predmet.leceni})`;
    } else {
        return `${predmet.nazev} (poškození ${predmet.poskozeni})`;
    }
}

for (const predmet of inventar) {
    console.log(popisPredmetu(predmet));
}

Vypíše:

Léčivý lektvar (léčí 20)
Stříbrný meč (poškození 12)

Uvnitř if je předmět typu Lektvar, protože druh: "lektvar" mají jen lektvary; ve větvi else je typu Zbran. Vlastnosti, které mají všechny typy ve sjednocení, tady druh a nazev, můžete číst bez kontroly.

Za běhu programu je značka obyčejná vlastnost: console.log(inventar[1]) vypíše { druh: "zbran", nazev: "Stříbrný meč", poskozeni: 12 }. Pro TypeScript je to ale klíč k celému objektu. Ví, které vlastnosti patří ke kterému druhu, a tak kontroluje i vaše data:

type Lektvar = { druh: "lektvar"; nazev: string; leceni: number };
type Zbran = { druh: "zbran"; nazev: string; poskozeni: number };
type Predmet = Lektvar | Zbran;

const korist: Predmet[] = [
    { druh: "lektvar", nazev: "Elixír", poskozeni: 5 },
    { druh: "lektvr", nazev: "Tonikum", leceni: 5 },
];
main.ts(6,41): error TS2353: Object literal may only specify known properties, and 'poskozeni' does not exist in type 'Lektvar'.
main.ts(7,7): error TS2820: Type '"lektvr"' is not assignable to type '"lektvar" | "zbran"'. Did you mean '"lektvar"'?

Lektvar s poškozením neprojde, protože Lektvar žádné poskozeni nemá. Neprojde ani druh s překlepem a TypeScript k němu dokonce napoví správnou hodnotu. Porovnání s překlepem, třeba predmet.druh === "lektvr", skončí chybou TS2367, kterou znáte: typy se nepřekrývají.

Typické chyby:

  • Objekt bez značky. const tonikum: Predmet = { nazev: "Tonikum", leceni: 5 }; skončí chybou error TS2322: Type '{ nazev: string; leceni: number; }' is not assignable to type 'Predmet'. a druhý řádek hlášky vysvětluje: Property 'druh' is missing in type '{ nazev: string; leceni: number; }' but required in type 'Lektvar'.
  • druh: string v typech místo literálového typu. Pak může být druh jakýkoli text, typy od sebe nerozliší a jeho kontrola nic nezúží: i po if (predmet.druh === "lektvar") je čtení predmet.leceni chyba.

Pokyny

Odměna za úkol je buď zlato, nebo předmět. Seznam v editoru už používá oba druhy, jenže TypeScript zatím nezná typ PredmetovaOdmena. Definujte ho pro předmětové odměny ze seznamu: druh: "predmet", nazev (text) a pocet (číslo).

Napište funkci popisOdmeny s parametrem odmena typu Odmena. Pro zlato vrátí třeba 50 zlatých, pro předmět počet a název, třeba 3x Léčivý lektvar. Vypište všechny odměny ze seznamu. Program pak vypíše:

50 zlatých
1x Stříbrný meč
3x Léčivý lektvar
30 zlatých

Sečtěte zlato ze všech zlatých odměn a součet vypište za odměnami:

50 zlatých
1x Stříbrný meč
3x Léčivý lektvar
30 zlatých
Zlato celkem: 80

Začněte programovat zdarma

Registrací souhlasíte s provozními podmínkami a zásadami ochrany osobních údajů.

Nebo se registrujte pomocí:

1/8