TypeScript w React bez bólu: 7 wzorców do produkcyjnych komponentów
Poznaj 7 praktycznych wzorców TypeScript w React: ComponentProps, discriminated unions, generics, satisfies, ref w React 19 i bezpieczne eventy.
Maciej Sala
Founder StriveLab
6 min czytaniaOpublikowano 24 kwietnia 2026 (Aktualizacja 26 czerwca 2026)
jest świetny, dopóki nie
trafisz na pierwszy komponent z forwardRef, generic props, polymorphic as prop albo komunikat „type instantiation is excessively deep". W tym momencie
większość deweloperów kopiuje rozwiązania ze Stack Overflow z 2020 roku — często
przestarzałe i niezgodne z React 19. Poniżej siedem wzorców, które sam stosuję
produkcyjnie i które rozwiązują te sytuacje raz a dobrze.
Kiedy używać konkretnych wzorców TypeScript w React?
Problem w komponencie
Najlepszy wzorzec
Co zyskujesz
Opakowujesz natywny element HTML
ComponentProps<'button'>
pełne propsy HTML, eventy i a11y bez przepisywania
Komponent ma kilka trybów działania
discriminated union
brak nielegalnych kombinacji propsów
Select albo hook działa na wielu typach
generics
inferencję typu danych bez duplikacji kodu
Konfiguracja ma pasować do schematu
satisfies / as const satisfies
walidację kształtu i wąskie typy tam, gdzie są potrzebne
Chcesz obsłużyć ref
ref jako prop w React 19 albo forwardRef w React 18
mniej boilerplate'u i poprawne typy referencji
Dane przychodzą z API, CMS-a albo storage
unknown + type guard / Zod
bezpieczeństwo także w runtime, nie tylko w kompilacji
Wzorzec 1: ComponentProps zamiast ręcznego definiowania propsów
Ręczne wypisywanie propów HTML elementu to najczęstszy błąd początkujących — i najbardziej kosztowny w utrzymaniu:
TypeScript infer'uje T automatycznie na podstawie items, więc zazwyczaj nie musisz pisać <User> explicitnie:
Code
<Select items={users} // T inferred as User getLabel={(user) => user.name} // user: User getValue={(user) => user.id} // user: User value={selectedUser} onChange={setSelectedUser}/>
Generics są nieocenione dla custom hooków, które zwracają typowane dane:
To nadal uproszczony przykład dla wartości, którym ufasz. Jeśli odczytujesz z
localStorage dane biznesowe, potraktuj je jak unknown i zwaliduj schematem
runtime, np. . TypeScript nie sprawdzi treści JSON-a,
który użytkownik albo inny skrypt zapisał w przeglądarce.
Wzorzec 4: z propem as
Popularny wzorzec — komponent, który może renderować się jako różny element HTML albo różny komponent. Przykład: <Button as="a" href="..."> vs <Button as="button">.
Polymorphic types są notorycznie trudne w TypeScript. Najbardziej robocze rozwiązanie w 2026:
TypeScript infer'uje propsy na podstawie as: dla as="a" dopuści href, a dla as={Link} dopuści propsy komponentu Link. Nie oznacza to jednak, że wymusi wszystkie reguły semantyczne HTML. W typach Reacta href na <a> jest opcjonalny, więc jeśli w Twoim design systemie link bez href ma być błędem, musisz dopisać osobny wariant typu albo test dostępności.
Ograniczenie: nie działa idealnie z forwardRef. Dla tego — patrz Wzorzec 6.
Wzorzec 5: satisfies dla const objects
TypeScript 4.9+ dodał operator satisfies, który jest game-changerem dla config objects:
Code
// Same as const zawęża wartości, ale nie sprawdza schematu tras.const ROUTES = { home: '/', about: '/about', blog: '/blog',} as const// Możesz użyć wartości jako stringa.const path: string = ROUTES.home // OK// TypeScript widzi też literalne wartości.type Path = (typeof ROUTES)[keyof typeof ROUTES]// Path = '/' | '/about' | '/blog'
Samo satisfies Record<string, string> sprawdzi, że wartości są stringami, ale
w takim układzie TypeScript może poszerzyć wartości do string. Jeśli chcesz
jednocześnie sprawdzić format i zachować literalne wartości, połącz oba
mechanizmy:
Różnica jest praktyczna: as const mówi „zawęź jak najmocniej", a satisfies
mówi „sprawdź zgodność z typem docelowym, bez przepisywania typu zmiennej na ten
typ". Gdy potrzebujesz i literalnych wartości, i walidacji kształtu, użyj
as const satisfies.
Dla typed color palettes, route maps, enum'ów — nieocenione:
Klasyczny problem — forwardRef nieprzyjemnie łączy się z generics i komponentami polymorphic. W React 19 nowy kod może częściej traktować ref jak zwykły prop:
Bez forwardRef kod jest prostszy, ale to dotyczy projektów faktycznie opartych o React 19 i aktualne typy. Jeśli nadal używasz forwardRef (React 18), składnia wygląda tak:
Jeśli masz React 18 w projekcie, migracja na React 19 i stopniowe usuwanie niepotrzebnego forwardRef może poprawić czytelność kodu. Jeśli jednak publikujesz bibliotekę komponentów, która ma wspierać React 18 i 19 jednocześnie, zostaw forwardRef w publicznym do czasu, aż świadomie porzucisz React 18.
Wzorzec 7: type-safe event handlers i eventy DOM
Najczęstszy problem — e.target.value. TypeScript często myśli, że e.target jest EventTarget (bez .value). Rozwiązanie:
Code
// ŹLE — TypeScript krzyczyfunction Input() { const handleChange = (e: Event) => { setValue(e.target.value) // Error: Property 'value' does not exist on 'EventTarget' }}// DOBRZE — typowane eventy Reactoweimport { ChangeEvent } from 'react'function Input() { const handleChange = (e: ChangeEvent<HTMLInputElement>) => { setValue(e.target.value) // OK, e.target jest HTMLInputElement } return <input onChange={handleChange} />}
Dla inline'ów TypeScript zwykle infer'uje z onChange={...}:
Code
<input onChange={(e) => setValue(e.target.value)} /> // e typed correctly
Typowe event typy:
ChangeEvent<T> — dla onChange na inputach, select'ach, textarea.
MouseEvent<T> — dla onClick, onMouseOver.
KeyboardEvent<T> — dla onKeyDown, onKeyUp.
FocusEvent<T> — dla onFocus, onBlur.
FormEvent<T> — dla onSubmit.
Gdzie T to element HTML (HTMLInputElement, HTMLButtonElement, etc.).
Dla custom event handlerów, które chcesz przekazać jako prop:
Code
type InputProps = { value: string onChange: (value: string) => void // nie event, tylko value}function Input({ value, onChange }: InputProps) { return <input value={value} onChange={(e) => onChange(e.target.value)} />}
Rozdzielenie „DOM event" od „business event" — komponent konwertuje eventy DOM na wygodniejsze API dla konsumenta.
Typy React i TypeScript, które warto znać
TypeScript ma gotowe helpery dla typowych sytuacji w React — wiele osób pisze je od zera, nie wiedząc, że już istnieją:
Code
// Props dla childrentype Props = { children: React.ReactNode // wszystko, co się da wyrenderować}// Props dla styletype Props = { style?: React.CSSProperties // typowany style object}// Handler dla event'u z komponentutype Props = { onClick: React.MouseEventHandler<HTMLButtonElement> onSubmit: React.FormEventHandler<HTMLFormElement>}// Ref do konkretnego HTML element'uconst divRef = useRef<HTMLDivElement>(null)// Setter dla useStatetype SetValueAction<T> = React.Dispatch<React.SetStateAction<T>>// Context z properti'amitype ThemeContextValue = { theme: string setTheme: (theme: string) => void}const ThemeContext = createContext<ThemeContextValue | null>(null)function useTheme() { const context = useContext(ThemeContext) if (!context) { throw new Error('useTheme must be used inside ThemeProvider') } return context}
Anti-patterns TypeScript w React, których warto unikać
Cztery nawyki, które najczęściej anulują wartość TypeScriptu w projekcie.
any wszędzie — tracisz całą wartość TypeScript. Jeśli nie znasz jeszcze kształtu danych, użyj unknown (musi być zawężony przed użyciem) albo wprowadź właściwy typ zamiast odkładać na później.
React.FC jako automatyczny odruch — nie jest błędem, ale w aktualnym React + TypeScript zwykle nie jest potrzebne. Zwykła funkcja z typowanymi propsami jest czytelniejsza, lepiej współpracuje z generics i zmusza Cię do jawnego podjęcia decyzji, czy komponent przyjmuje children.
Casting as — kiedykolwiek piszesz x as SomeType, zatrzymaj się i zastanów. W 90% przypadków lepszym rozwiązaniem jest type guard lub węższy typ na początku, nie kasowanie błędu rzutowaniem.
Zbyt szerokie typy — string zamiast 'small' | 'medium' | 'large', Record<string, unknown> zamiast konkretnego obiektu. Każde zawężenie to potencjalny błąd złapany w compile time, zanim dotrze do użytkownika.
Brak walidacji runtime — TypeScript kończy pracę w kompilatorze. Dane z API, CMS-a, formularza, URL-a i localStorage nadal mogą mieć zły kształt, dlatego na granicach systemu potrzebujesz type guardów albo schematów runtime.
Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.
Nie, ale praktycznie każdy produkcyjny projekt w 2026 używa TS. Dla solo-dev'ów JavaScript może wystarczyć, dla zespołów TS jest standardem. Koszt wejścia mały, zysk w compile-time safety ogromny.
`React.FC` czy zwykła funkcja?
Zwykła funkcja jako domyślny styl. React.FC jest poprawne, ale w aktualnych typach Reacta zwykle nie daje przewagi nad jawnym typowaniem propsów, a przy komponentach generycznych bywa mniej wygodne. children dodawaj jawnie przez children?: React.ReactNode albo PropsWithChildren.
Dlaczego `as const` i `satisfies` nie są tym samym?
as const zawęża wartość do literałów i robi obiekt readonly. satisfies sprawdza zgodność z typem docelowym, ale nie zawsze samo zawęża stringi do literałów. Dla map tras często używasz obu naraz: as const satisfies Record<string, \/${string}\>.
Jak najlepiej typować custom hooki?
Zwracaj tuple (return [value, setValue] as const) dla hooków podobnych do useState lub obiekt (return { data, isLoading, error }) dla hooków podobnych do useQuery. as const jest kluczowe dla tuple'i, żeby TypeScript infer'ował typy pozycyjnie.
Czy TypeScript zastępuje PropTypes?
W typowaniu propsów aplikacji zwykle tak, ale tylko w compile time. TypeScript nie waliduje danych w runtime, więc dla API, CMS-a, formularzy i localStorage nadal potrzebujesz walidacji runtime, np. Zod.
O autorze
Maciej Sala
Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.
Walka z nadmiarowymi renderami to stały punkt pracy z Reactem. O ile podwójne renderowanie w Strict Mode czy standardowa aktualizacja stanu rodzica to zachowania całkowicie naturalne, o tyle lawinowe przebudowywanie drzewa komponentów po jednym kliknięciu potrafi skutecznie zepsuć UX. React Profiler pomoże rozwiązać te problemy dostarczając twarde dane o wydajności Twojej aplikacji.
Maciej Sala
Founder StriveLab
React 19 pozwala przekazać funkcję bezpośrednio do atrybutu action elementu . Dzięki integracji z Actions, useActionState , useFormStatus i useOptimistic możesz ograniczyć ręczną obsługę onSubmit , pending state oraz optymistycznych aktualizacji , zachowując kontrolę nad błędami i stanem trwałym.
Maciej Sala
Founder StriveLab
INP ostatecznie odesłał FID na emeryturę, wymuszając zupełnie nowe spojrzenie na responsywność. Metryka ocenia teraz najgorszą interakcję z całej sesji, bez taryfy ulgowej dla aplikacji React wypełnionych bogatym stanem, potężnymi listami i rozbudowanymi zdarzeniami. Jest jednak kilka powtarzalnych wzorców, które radzą sobie z większością problemów z optymalizacją, z których każdy posiada gotowe rozwiązanie systemowe.