Lekce 12 · Ekosystém
TypeScript s Reactem
Typování props, událostí, hooků, generické komponenty a discriminated unions.
Načítám lekci…
Lekce 12 · Ekosystém
Typování props, událostí, hooků, generické komponenty a discriminated unions.
Načítám lekci…
Do české zásuvky nestrčíš britskou zástrčku. Nemusíš nic zapojovat ani zkoušet, jestli to vyhodí pojistky – je to poznat už podle tvaru. Tvar zásuvky je vlastně smlouva: „sem patří tohle, a nic jiného“.
Typ props je přesně takový tvar zásuvky. Komponenta řekne: „potřebuju jméno (text), roli (buď admin, nebo user) a funkci, kterou zavolám po kliknutí“. Kdo ji použije s chybějícím jménem nebo s rolí „admn“, dozví se to v editoru, ještě před spuštěním – červenou vlnovkou, ne chybou v prohlížeči u zákazníka.
V minulé lekci jsme se naučili, kam se štítky s typy lepí. Teď je nalepíme na věci, které jsou typické pro React: props, události, hooky, kontext a komponenty, které pracují s libovolnými daty.
Karta uživatele, která ukazuje jméno, roli a po kliknutí dá vědět rodiči:
interface UserCardProps {id: numbername: stringage?: number // nepovinnérole: 'admin' | 'user' // jen jedna z těchto dvou hodnotonSelect: (id: number) => void // funkce, kterou karta zavoláchildren?: ReactNode // obsah mezi tagy}function UserCard({ id, name, age = 18, role, onSelect, children }: UserCardProps) {return (<div onClick={() => onSelect(id)}><strong>{name}</strong> ({age}) – {role}{children}</div>)}
Co za nás TypeScript ohlídá:
<UserCard id={1} name="Jana" role="admin" onSelect={select} /> // ✅<UserCard id={1} role="admin" onSelect={select} />// ❌ Property 'name' is missing – chybí povinný prop<UserCard id={1} name="Jana" role="admn" onSelect={select} />// ❌ Type '"admn"' is not assignable to type '"admin" | "user"' – překlep<UserCard id={1} name="Jana" role="user" onSelect={(id: string) => …} />// ❌ onSelect dostane number, ne string
'admin' | 'user') místo obecného string. Editor ti při psaní nabídne obě možnosti a překlep nepustí.age vždy číslo.void = nic užitečného).React nabízí několik vlastních typů, které se v props hodí:
| Typ | Kdy |
|---|---|
ReactNode | children a sloty – text, číslo, JSX, pole, null… cokoli vykreslitelného. |
ReactElement | Když potřebuješ právě jeden element (vzácné). |
ComponentProps<'button'> | Všechny props nativního elementu. |
ComponentProps<typeof MyComp> | Props existující komponenty (i z knihovny, která je neexportuje). |
CSSProperties | Objekt pro prop style. |
Píšeš vlastní Button s variantami barev. Uživatel ale bude chtít předat i disabled, type="submit", aria-label, title… Vypisovat je všechny ručně je nesmysl. Vezmeš proto tvar nativní zásuvky a přidáš k němu svůj:
type ButtonProps = ComponentProps<'button'> & {variant?: 'primary' | 'ghost' | 'danger'}function Button({ variant = 'primary', className = '', ...rest }: ButtonProps) {return <button className={`btn btn-${variant} ${className}`} {...rest} />}
ComponentProps<'button'> = všechny props, které umí obyčejný <button>.& = „a zároveň“. Výsledek má nativní props i naši variantu....rest posbírá všechno, co jsme si nevzali, a pošle to dál na <button>. Včetně onClick, disabled i ref.Na poště vyplňuješ formulář. Nahoře zaškrtneš, jestli posíláš dopis, nebo balík – a podle toho vyplníš jiná políčka. U balíku váhu a rozměry, u dopisu nic z toho. Kdo zaškrtne dopis a vyplní váhu, něco pokazil.
Komponenta, která je buď odkaz, nebo tlačítko s akcí, má stejný problém. S obyčejnými nepovinnými props jde zapsat nesmysl:
// ❌ Nepovinné obojí → jde předat obojí, nebo nicinterface Props { href?: string; onClick?: () => void; children: ReactNode }// ✅ „Zaškrtávací políčko“ kind rozhodne, co dalšího je povinnétype LinkOrActionProps =| { kind: 'link'; href: string; children: ReactNode }| { kind: 'action'; onClick: () => void; children: ReactNode }function LinkOrAction(props: LinkOrActionProps) {if (props.kind === 'link') {return <a href={props.href}>{props.children}</a> // TS ví: tady je href}return <button onClick={props.onClick}>{props.children}</button> // a tady onClick}
Uvnitř if TypeScript ví, kterou variantu máš, a pustí tě jen k jejím vlastnostem. A zvenku nejde napsat kind="link" bez href.
Co vidíš: Nahoře tři tlačítka vlastní komponenty Button – s ikonou, vypnuté s nativním title, a jen s ikonou a aria-label. Dole dvě varianty LinkOrAction.
Vyzkoušej:
title – prošel přes ...rest, aniž bychom ho museli vypisovat.kind="link" a onClick. Kdybys ho odkomentoval, TypeScript ohlásí chybu.Co z toho plyne: Nativní props zdědíš jedním řádkem. Discriminated union zaručí, že komponenta dostane vždy smysluplnou kombinaci.
Načítám interaktivní část…
Když napíšeš handler přímo do JSX, TypeScript ví, ke kterému prvku a události patří, a typ e odvodí sám. Když handler vytáhneš do samostatné proměnné, tuhle souvislost ztratí – a typ musíš napsat. Je to neprůhledná krabice z minulé lekce.
// Inline handler: typ se odvodí sám – nejjednodušší varianta<input onChange={(e) => setName(e.target.value)} />// Samostatný handler: typ události + typ prvku ve špičatých závorkáchconst handleChange = (e: ChangeEvent<HTMLInputElement>) => setName(e.target.value)const handleSubmit = (e: FormEvent<HTMLFormElement>) => { e.preventDefault() }const handleKey = (e: KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter') … }const handleClick = (e: MouseEvent<HTMLButtonElement>) => { e.currentTarget.disabled = true }// Nebo typ celého handleru najednouconst onClick: MouseEventHandler<HTMLButtonElement> = (e) => { … }
U hooků platí pravidlo průhledných krabic: když TypeScript vidí počáteční hodnotu a je z ní všechno jasné, typ nepiš. Když je krabice prázdná nebo „zatím nic“, typ předej ve špičatých závorkách.
useState(0) // number – průhledná krabiceuseState<User | null>(null) // teď nic, později User – musíš uvéstuseState<Product[]>([]) // prázdné pole – musíš uvést (jinak never[])useRef<HTMLInputElement>(null) // ref na prvek stránkyuseRef<number | null>(null) // šuplík na ID časovačeuseRef(0) // šuplík s číslem – odvozenouseReducer(reducer, initial) // typy se odvodí z reducerucreateContext<AuthValue | null>(null) // + vlastní hook s kontrolou null// Vlastní hook vracející dvojicifunction useToggle() {…return [on, toggle] as const // readonly [boolean, () => void]}
U useReducer stačí pořádně otypovat reducer – stav a akce jako discriminated union z lekce o reduceru. Všechno ostatní (typ state, povolené akce v dispatch) se z něj odvodí.
Prodejní automat neví předem, co v něm bude. Když ho naplníš plechovkami, vydává plechovky; když sušenkami, sušenky. A nevydá ti sušenku z automatu s plechovkami. Generická komponenta funguje stejně: neví předem, s jakými daty bude pracovat, ale jakmile jí je dáš, „naučí se“ jejich typ a hlídá ho.
Typický příklad: tabulka, která umí zobrazit produkty, uživatele, objednávky…
interface Column<T> {key: keyof T & string // jen skutečná vlastnost Theader: stringrender?: (row: T) => ReactNode // řádek přijde správně otypovaný}interface TableProps<T> {rows: T[]columns: Column<T>[]}function Table<T>({ rows, columns }: TableProps<T>) { … }
<T> za názvem funkce říká: „tahle komponenta má parametr typu, zatím ho neznám“.<Table rows={products} … />, TypeScript se podívá na rows a zjistí, že T je Product. Automat je naplněný.key ve sloupcích být vlastnost produktu ('name', 'price'…) a funkce render dostane Product.<Tablerows={products}columns={[{ key: 'name', header: 'Název' },{ key: 'price', header: 'Cena', render: (p) => `${p.price} Kč` }, // p je Product{ key: 'color', header: 'Barva' }, // ❌ 'color' není vlastnost Product]}/>
Co vidíš: Jedna komponenta Table použitá dvakrát: s produkty a s uživateli. Sloupce s šipkou jdou řadit.
Vyzkoušej:
color. Produkt žádnou barvu nemá – kdybys řádek odkomentoval, TypeScript ho podtrhne.Co z toho plyne: Komponenta je napsaná jednou pro libovolná data. Typ dat se odvodí z rows a hlídá sloupce i funkce render.
Načítám interaktivní část…
| Zápis | K čemu |
|---|---|
const sizes = ['S', 'M'] as const | Readonly dvojice textů → typeof sizes[number] = "S" | "M". |
obj satisfies Record<Tone, string> | Ověří tvar, ale zachová přesný typ hodnoty. |
Omit<ButtonProps, "type"> | Props bez některých klíčů. |
Partial<T>, Required<T>, Pick<T, K> | Odvozené typy bez kopírování. |
const x: never = action | Kontrola úplnosti ve switchi. |
z.infer<typeof schema> | Typ ze Zod schématu – validace i typy z jednoho zdroje. |
Typ props je napsaný – komponenta ho jen musí použít.
VÝZVA: Štítek s typovanými props
Typ props (BadgeProps) je hotový. Komponenta ho zatím nepoužívá.
1. Místo (props: BadgeProps) rozbal props přímo v parametru: function Badge({ label, tone = 'ok' }: BadgeProps)
2. Vykresli <span className={tone}>{label}</span>
(třídy ok a bad jsou hotové styly kurzu – zelená a červená)row, stack, card, list, btn, input nebo muted jsou hotové styly kurzu v src/styles.css (row = prvky vedle sebe, stack = pod sebou, card = rámeček, muted = šedý text).Načítám interaktivní část…
První výzva procvičí kapitoly 1, 2 a 5: tvary zásuvek pro props, rozšíření nativního tlačítka a generický Select. Druhá kapitolu 4 – typovaný reducer a kontext, ve kterém překlep v názvu akce odhalíš dřív, než uložíš soubor.
UI vypadá stejně – změna je jen v typech. Úspěch pozná až TypeScript.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
Discriminated union pro akce + typovaný kontext = překlep v názvu akce odhalíš dřív, než uložíš soubor.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
string.ComponentProps<'button'> & {…} pro rozšíření nativních prvků, ReactNode pro children.ChangeEvent<…> apod.useState<T>() u null, unií a prázdných polí; jinak nech TS odvodit.