Menu

React z TypeScriptem: typowanie propsów, stanu i zdarzeń

Jak otypować aplikację React w TypeScripcie: propsy i children, useState z uniami i null, useRef dla elementów DOM, handlery zdarzeń, kontekst, komponenty generyczne i propsy natywnych elementów.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

React z TypeScriptem oznacza pisanie komponentów w plikach .tsx, w których propsy, stan, refy i handlery zdarzeń mają typy, więc kompilator wychwytuje brakujący prop albo zły handler zdarzenia, zanim kod się uruchomi. Typy znikają w czasie budowania: w przeglądarce działa ten sam JavaScript, który napisałbyś bez nich. Żywe edytory na tej stronie uruchamiają ten JavaScript, a wersja z typami stoi obok każdego z nich w statycznym bloku.

Oto ten sam Badge z typami. count jest opcjonalne (?), więc drugi badge wraca do wartości domyślnej 0, a children przyjmuje zarówno element, jak i zwykły tekst.

type BadgeProps = {
    label: string;
    count?: number;
    children: React.ReactNode;
};

function Badge({ label, count = 0, children }: BadgeProps) {
    return (
        <div>
            <strong>{label}</strong> ({count})
            <div>{children}</div>
        </div>
    );
}

<Badge count={3}>Hi</Badge>;
// Error: Property 'label' is missing in type '{ children: string; count: number; }'

Tworzenie projektu z TypeScriptem

Vite ma szablon React z TypeScriptem:

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

Dostajesz pliki .tsx, tsconfig.json z włączonym strict i już zainstalowane definicje typów Reacta (@types/react, @types/react-dom). Vite usuwa typy podczas serwowania i budowania, ale ich nie sprawdza; skrypt build z szablonu najpierw uruchamia tsc, a twój edytor sprawdza typy w trakcie pisania. Frameworki takie jak Next.js konfigurują TypeScript tak samo, gdy tworzysz projekt ich CLI.

Typowanie propsów

Opisz propsy jako typ obiektu i oznacz typem zdestrukturyzowany parametr. Kilka wzorców obejmuje większość komponentów:

interface UserCardProps {
    name: string;                       // required
    age?: number;                       // optional, may be undefined
    role: 'admin' | 'member';           // a union of allowed strings
    tags: string[];
    onSelect: (id: string) => void;     // a callback prop
    children?: React.ReactNode;         // anything React can render
}

function UserCard({ name, age, role, tags, onSelect, children }: UserCardProps) {
    // ...
}

type czy interface. Oba działają dla propsów. interface można rozszerzać przez extends i scalać, deklarując go dwa razy; type może też wyrażać unie, przecięcia i typy mapowane. Większość zespołów wybiera jedno i używa go wszędzie.

children. Używaj React.ReactNode dla wszystkiego, co trafia między tagi: elementów, tekstów, liczb, tablic, null. Strona o propsach wyjaśnia, jak children w ogóle trafia do komponentu.

Dlaczego React.FC jest opcjonalne. const Badge: React.FC<BadgeProps> = (...) => ... typuje całą funkcję zamiast parametru. Od typów React 18 nie dodaje już za ciebie children, nie może przyjąć parametru typu dla komponentu generycznego i nie dodaje niczego, czego nie daje otypowany parametr. Starszy kod używa go powszechnie; dla nowego kodu częstym wyborem są zwykłe funkcje z otypowanymi propsami.

Typowanie useState

Przy prostej wartości początkowej wystarczy wnioskowanie: useState(0) to number, useState('') to string. Przekaż argument typu, gdy stan może zawierać więcej, niż sugeruje wartość początkowa: wartość, która zaczyna jako null, albo jeden z kilku stałych tekstów.

Kliknij "Load user", a status przejdzie od idle przez loading do done. Stan z typami:

type User = { id: number; name: string };
type Status = 'idle' | 'loading' | 'done';

const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<Status>('idle');

user.name;          // Error: 'user' is possibly 'null'
user?.name;         // OK
setStatus('ready'); // Error: Argument of type '"ready"' is not assignable to parameter of type 'SetStateAction<Status>'

Bez <User | null> useState(null) wywnioskuje typ null, a setUser(data) się nie skompiluje. Unia zmusza cię do obsłużenia pustego przypadku, czyli dokładnie sprawdzenia user ? ... : ... w działającym przykładzie.

useRef dla elementów DOM

Ref, który wskazuje na element DOM, przyjmuje typ elementu i zaczyna jako null, bo element nie istnieje, dopóki React go nie podłączy po pierwszym renderowaniu.

Wpisz coś, a potem kliknij "Log the value": konsola wypisze to, co wpisałeś, odczytane prosto z elementu DOM. Wersja z typami:

const inputRef = useRef<HTMLInputElement>(null);
// inputRef.current is HTMLInputElement | null

inputRef.current?.focus();    // OK
inputRef.current.focus();     // Error: 'inputRef.current' is possibly 'null'

const timerId = useRef<number | null>(null);  // a mutable value, not a DOM node

Używaj interfejsu elementu, który odpowiada tagowi: HTMLInputElement, HTMLDivElement, HTMLCanvasElement, HTMLButtonElement. W typach React 19 useRef zawsze przyjmuje argument i zwraca RefObject, do którego current możesz przypisywać, więc ta sama forma działa dla refów do DOM i dla wartości takich jak ID timera. Stronę działania w czasie wykonania opisuje useRef.

Typy handlerów zdarzeń

Handlery w linii nie potrzebują adnotacji: w onChange={(e) => setName(e.target.value)} TypeScript zna e z propsa, do którego jest przekazany. Dodawaj adnotację, gdy przenosisz handler do osobnej funkcji.

Wyślij formularz, a konsola wypisze adres; preventDefault zatrzymuje przeładowanie strony. Z typami:

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    setEmail(e.target.value); // e.target is HTMLInputElement, value is string
}

function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
    e.preventDefault();
}

function handleClick(e: React.MouseEvent<HTMLButtonElement>) {}
function handleKey(e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.key === 'Enter') { /* ... */ }
}

Od @types/react 19.2.10 onSubmit jest typowane przez React.SubmitEvent, a starsze React.FormEvent jest oznaczone jako przestarzałe; przy starszej wersji typów napisz zamiast tego React.FormEvent<HTMLFormElement>. Parametr typu to element, do którego podłączony jest handler. Jeśli nie znasz typu, najedź w edytorze na prop onChange albo otypuj cały handler naraz: const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { ... }.

Typowanie kontekstu

Podaj createContext typ wartości. Jeśli istnieje sensowna wartość domyślna, przekaż ją, a każdy konsument dostanie wartość różną od null:

type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');

const theme = useContext(ThemeContext); // Theme

Gdy nie ma dobrej wartości domyślnej (zalogowany użytkownik, magazyn danych), zacznij od null i owiń useContext w hook, który rzuca błąd, jeśli komponent jest używany poza providerem. Każdy wywołujący dostaje wtedy typ różny od null i czytelny komunikat o błędzie zamiast awarii gdzieś głęboko w środku.

type Auth = { user: string; logout: () => void };
const AuthContext = createContext<Auth | null>(null);

export function useAuth(): Auth {
    const value = useContext(AuthContext);
    if (!value) throw new Error('useAuth must be used inside <AuthProvider>');
    return value;
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
    const [user, setUser] = useState('Ada');
    return (
        <AuthContext value={{ user, logout: () => setUser('') }}>
            {children}
        </AuthContext>
    );
}

<AuthContext value={...}> to składnia providera z React 19; <AuthContext.Provider value={...}> jest typowany tak samo.

Komponenty generyczne

Lista, tabela albo select, które działają z dowolnym typem elementu, to komponent generyczny: parametr typu łączy prop items z callbackiem renderItem.

List z typami:

type ListProps<T> = {
    items: T[];
    renderItem: (item: T) => React.ReactNode;
    getKey: (item: T) => string | number;
};

function List<T>({ items, renderItem, getKey }: ListProps<T>) {
    return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}

<List items={books} getKey={(b) => b.isbn} renderItem={(b) => b.title} />;
// b is inferred as the book type, so b.titel would be an error

TypeScript wnioskuje T z items, więc wywołujący nigdy nie piszą parametru typu. W pliku .tsx funkcja strzałkowa potrzebuje <T,> zamiast <T>, bo samo <T> czyta się jak tag JSX.

Rozszerzanie propsów natywnych elementów

Opakowanie wokół button albo input powinno przyjmować każdy atrybut, który przyjmuje natywny element. React.ComponentProps<'button'> to ten pełny zestaw; dodaj na wierzchu własne propsy, a resztę rozpakuj.

type ButtonProps = React.ComponentProps<'button'> & {
    variant?: 'primary' | 'ghost';
};

function Button({ variant = 'primary', style, ...rest }: ButtonProps) {
    return (
        <button
            {...rest}
            style={{ fontWeight: variant === 'primary' ? 600 : 400, ...style }}
        />
    );
}

<Button type="submit" disabled onClick={(e) => console.log(e.currentTarget)}>
    Save
</Button>;

onClick, disabled, type i aria-* są otypowane bez wymieniania ich. W React 19 ref to zwykły prop, a ComponentProps<'button'> go zawiera, więc <Button ref={buttonRef}> dociera do węzła DOM przez spread bez forwardRef. Używaj ComponentPropsWithoutRef<'button'>, gdy chcesz pominąć ref, a ComponentProps<typeof UserCard>, żeby odczytać propsy jednego z własnych komponentów.

Częste błędy typów

"'ref.current' is possibly 'null'". Element nie istnieje podczas pierwszego renderowania. Użyj ref.current?.focus() albo przed użyciem sprawdź if (ref.current).

"Type 'string' is not assignable" przy stanie będącym unią. const [status, setStatus] = useState('idle') wnioskuje string, czyli szerszy typ, niż chcesz, a przekazanie go do propsa typu Status się nie udaje. Napisz useState<Status>('idle').

Zdarzenie otypowane jako zły element. React.ChangeEvent<HTMLInputElement> na select sprawia, że e.target ma zły typ. Parametr typu musi odpowiadać elementowi, do którego podłączony jest handler.

Używanie any, żeby uciszyć błąd. Wyłącza sprawdzanie dla wszystkiego, czego dotyka ta wartość. Wybieraj unknown i zawężaj go albo napraw typ u źródła.

Najczęściej zadawane pytania

Jak utworzyć aplikację React z TypeScriptem?

Uruchom npm create vite@latest my-app -- --template react-ts, potem npm install i npm run dev. Komponenty trafiają do plików .tsx, a Vite usuwa typy podczas budowania; żeby je sprawdzić, uruchom tsc (robi to skrypt build z szablonu).

Jaki jest typ children w React?

React.ReactNode. Przyjmuje wszystko, co React potrafi wyrenderować: elementy, teksty, liczby, tablice, null i undefined. React.ReactElement używaj tylko wtedy, gdy potrzebujesz dokładnie jednego elementu.

Jak otypować useState z null?

Przekaż typ jawnie jako unię: useState<User | null>(null). Bez argumentu typu TypeScript wywnioskuje null jako jedyną dozwoloną wartość.

Jaki jest typ zdarzenia onChange w React?

React.ChangeEvent<HTMLInputElement> dla pola input (HTMLTextAreaElement albo HTMLSelectElement dla tych elementów). e.target.value jest wtedy otypowane jako string.

Czy używać React.FC?

To opcjonalne. Bezpośrednie typowanie parametru propsów (function Card({ title }: CardProps)) robi to samo, czyta się prościej i działa z generykami. React.FC nie dodaje już niejawnie children, więc daje niewiele więcej.

Czy dla propsów używać type, czy interface?

Oba działają. interface można rozszerzać i scalać; type może wyrażać unie i typy mapowane. Wybierz jedną konwencję dla całego projektu i się jej trzymaj.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ