TypeScript 02 · Základy TypeScriptu
Typy, které se mění (union a zúžení)
Union typy, zúžení podmínkou, literálové typy, discriminated union s kontrolou úplnosti a unknown pro data zvenku.
Načítám lekci…
TypeScript 02 · Základy TypeScriptu
Union typy, zúžení podmínkou, literálové typy, discriminated union s kontrolou úplnosti a unknown pro data zvenku.
Načítám lekci…
Kurýr ti přinese balík a na štítku stojí: „kniha, nebo hrnek“. Dokud ho neotevřeš, nemůžeš v něm listovat ani z něj pít – nevíš, co v něm je. Jakmile ho otevřeš a podíváš se dovnitř, víš to jistě a od té chvíle s ním zacházíš podle obsahu.
Přesně takhle pracuje TypeScript s hodnotami, které můžou být víc věcí. Typ string | number je štítek „text, nebo číslo“. Dokud se nepodíváš (podmínkou), dovolí ti s hodnotou jen to, co jde u obou možností. Po kontrole ví, co v balíku je, a pustí tě k metodám, které k obsahu patří.
V lekci TypeScript 01 šlo o to, kam se typy píšou. Tahle lekce je o typech, které popisují víc možností – a o tom, jak TypeScript z tvého kódu pozná, která z nich právě platí. V Reactu je potkáš na každém kroku: uživatel, nebo zatím nic; stav načítání; data ze serveru, kterým zatím nevěříš.
Svislá čára mezi typy znamená „nebo“. Takovému typu se říká union (sjednocení):
let id: string | number // text, nebo čísloid = 42 // ✅id = 'A-42' // ✅id = true // ❌ boolean ve štítku nenílet selected: User | null = null // uživatel, nebo zatím nic
Union z null už znáš: useState<User | null>(null) z minulé lekce. Teď to hlavní: s hodnotou typu union smíš dělat jen to, co jde u všech možností. TypeScript neví, co v balíku zrovna je, a tak hlídá nejhorší případ.
function show(id: string | number) {id.toString() // ✅ toString má text i čísloid.toUpperCase() // ❌ Property 'toUpperCase' does not exist on type 'number'}
Chyba neříká „tohle je špatně“, ale „tohle nemusí vyjít“: kdyby přišlo číslo, toUpperCase by neexistovalo a aplikace by spadla. Řešením je balík nejdřív otevřít – to je další kapitola.
Když v kódu napíšeš podmínku, která typ prověří, TypeScript ji přečte a uvnitř větve s hodnotou zachází jako s užším typem. Tomu se říká zúžení (narrowing). Nemusíš nic deklarovat navíc – stačí obyčejný JavaScript:
function greet(name: string | null) {if (name === null) return 'Ahoj!'// sem se null nedostane – funkce by skončila o řádek výšreturn `Ahoj, ${name.toUpperCase()}!` // ✅ name je tady string}
Kontrol, kterým TypeScript rozumí, je víc. Tyhle použiješ nejčastěji:
| Kontrola | Na co | Příklad |
|---|---|---|
typeof x === 'string' | text, číslo, boolean, funkce | if (typeof id === 'number') id.toFixed(2) |
x === null / x !== null | odlišení „nic“ od hodnoty | if (user !== null) user.name |
Array.isArray(x) | pole, nebo jedna hodnota | const list = Array.isArray(x) ? x : [x] |
'klíč' in x | objekt s touhle vlastností | if ('email' in contact) contact.email |
x instanceof Error | objekty z tříd (Error, Date) | if (e instanceof Error) e.message |
x.status === 'error' | jeden z několika tvarů objektu (kapitola 4) | if (state.status === 'error') state.message |
Funguje to i přes return: co už „odešlo“ z funkce, z typu zmizí. Proto se vyplatí nejdřív vyřídit okrajové případy (null, chybu) a zbytek funkce psát pro ten normální.
Co vidíš: Krátká funkce, která dostane text, číslo, nebo null. Každý řádek je tlačítko; pod kódem je vidět, jaký typ má value právě na tom řádku.
Vyzkoušej:
return 'nic': typ se zúží, i když tam žádná podmínka není.return zbyde jen number – proto tam smí být toFixed.Co z toho plyne: Typ hodnoty se během funkce mění podle toho, co už víš. Každá podmínka a každý return odebere z unionu možnosti, které už nastat nemůžou.
Načítám interaktivní část…
Typ nemusí být jen string nebo number. Může to být i jedna konkrétní hodnota – a z několika takových se skládá union:
type Theme = 'light' | 'dark'type Size = 'sm' | 'md' | 'lg'function setTheme(theme: Theme) { … }setTheme('dark') // ✅setTheme('Dark') // ❌ překlep TypeScript pozná hnedsetTheme('modrá') // ❌ takové téma neexistuje
Je to jako formulář s políčky k zaškrtnutí místo prázdné řádky: kdo ho vyplňuje, nemůže napsat nesmysl. Editor navíc nabízí povolené hodnoty, jakmile napíšeš uvozovku. U props komponent je to k nezaplacení: variant: 'primary' | 'secondary' řekne každému, kdo komponentu použije, co smí předat.
Načítání dat má několik stavů: zatím nic, načítá se, hotovo, chyba. Nabízí se popsat je pár vlastnostmi vedle sebe – a to je past:
// ❌ takhle neinterface State {isLoading: booleanerror?: stringuser?: User}// TypeScript klidně povolí nesmysl:const state: State = { isLoading: true, error: 'Síť nefunguje', user: jana }// Načítá se? Je chyba? Máme uživatele? Všechno najednou?
Lepší je popsat každý stav zvlášť a spojit je do unionu. Všechny tvary mají společnou vlastnost s literálovým typem (tady status), podle které se dají rozlišit. Té se říká diskriminátor a celému typu discriminated union:
// ✅ takhle anotype State =| { status: 'idle' }| { status: 'loading' }| { status: 'success'; user: User }| { status: 'error'; message: string }
Uživatele má jen stav 'success', chybovou zprávu jen 'error'. Nesmyslná kombinace se nedá ani zapsat. A když se zeptáš na status, TypeScript zúží celý objekt:
function render(state: State) {switch (state.status) {case 'idle':return 'Klikni na Načíst'case 'loading':return 'Načítám…'case 'success':return state.user.name // ✅ tady user existujecase 'error':return state.message // ✅ tady message existuje}}
Co když za měsíc někdo přidá stav { status: 'cancelled' }? Funkce výše ho tiše přeskočí. Proti tomu je jednoduchý trik: za switch přidej řádek, na který se kód dostane, jen když nějaký stav chybí.
switch (state.status) {// … všechny case}const unreachable: never = state // ❌ chyba, jakmile nějaký stav chybíreturn unreachable
never je typ „tohle nastat nemůže“. Když switch vyřídí všechny stavy, zbyde za ním opravdu nic a přiřazení projde. Chybí-li některý stav, zůstane v state a TypeScript ohlásí chybu přesně na tomhle řádku – dřív, než na zapomenutý stav narazí uživatel.
Co vidíš: Tři tlačítka načítají uživatele z falešného API. Pod nimi je, co komponenta vykreslí, a ještě níž aktuální objekt stavu.
Vyzkoušej:
status, pak i user.message, ale žádné user.StateView. V každém case sahá jen na vlastnosti, které daný stav má.Co z toho plyne: Stav je vždy jen jeden z tvarů unionu. Komponenta se podle status rozhodne, co ukázat, a TypeScript jí přitom hlídá, aby nesáhla na data, která v daném stavu nejsou.
Načítám interaktivní část…
Některé hodnoty přicházejí odněkud, kam TypeScript nevidí: z JSONu, ze serveru, z localStorage, z bloku catch. Jsou to balíky bez štítku. TypeScript pro ně má dva typy a liší se jako noc a den:
| any | unknown | |
|---|---|---|
| Co znamená | „nekontroluj mě“ | „nevím, co to je“ |
| Co s hodnotou smíš | cokoli – i nesmysl | nic, dokud ji neprověříš |
| Kdy se chyba ukáže | až za běhu, u uživatele | hned v editoru |
| Kde ho potkáš | JSON.parse, response.json() – vracejí any | catch (e), hodnoty, které si sám označíš |
any kontrolu vypne a dál se šíří: co z něj vznikne, je zase any. unknown je jeho bezpečný bratr – nic nedovolí, dokud hodnotu nezúžíš. Proto si výsledek z JSON.parse ulož jako unknown:
const data: unknown = JSON.parse(text)data.theme // ❌ 'data' is of type 'unknown'if (typeof data === 'object' && data !== null && 'theme' in data) {data.theme // ✅ vlastnost existuje (její hodnotu ještě ověř)}
Psát takovou podmínku pokaždé znovu by bylo otravné. Zabal ji do funkce s návratovým typem x is User. Takové funkci se říká typová stráž (type guard): když vrátí true, TypeScript ví, že hodnota je User.
function isUser(x: unknown): x is User {return typeof x === 'object' && x !== null && 'name' in x && typeof x.name === 'string'}const data: unknown = await response.json()if (isUser(data)) {data.name.toUpperCase() // ✅ tady je data User}
Stejně se zachází s chybou v catch. Vyhodit se dá cokoli (i text nebo číslo), proto má chyba typ unknown:
try {await saveProfile()} catch (e) {const message = e instanceof Error ? e.message : 'Neznámá chyba'}
Dvě funkce, které dostanou hodnotu dvojího typu. Nejdřív zjisti, co přišlo, a pak vrať správný výsledek.
Zadání najdeš v komentářích v kódu níže – na začátku souboru a u míst označených TODO.
Načítám interaktivní část…
Bezpečné čtení nastavení uloženého jako JSON: typová stráž, převod s ošetřením chyb a text chyby z catch. Co má která funkce dělat, je v komentářích v kódu.
Zadání najdeš v komentářích v kódu níže – na začátku souboru a u míst označených TODO.
Načítám interaktivní část…
Čtyři stavy načítání jako discriminated union: text pro každý stav s kontrolou úplnosti, bezpečné čtení jména a přechody mezi stavy. Co má která funkce dělat, je v komentářích v kódu.
Střední a těžké výzvy se odemknou, jakmile se přihlásíš. Stejně jako celý kurz jsou zdarma – stačí e-mail, žádné heslo ani platba.
A | B znamená „A, nebo B“. Bez kontroly smíš s hodnotou dělat jen to, co jde u všech možností.typeof, === null, in, instanceof) i return a uvnitř větve zná užší typ.'light' | 'dark') místo obecného textu – překlep pozná editor. Místo enum stačí union textů.status. Nesmyslná kombinace nejde zapsat; never za switch pohlídá, že žádný stav nechybí.any. Prověř je typovou stráží x is T; as nic nekontroluje.