StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

StriveLab
Strony internetowe
Usługi
RealizacjeO mnieBlogPorozmawiajmy
PL
EN

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

Astro

Ultraszybkie projekty, łączące lekkość ze skalowalnością.

Next.js

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.

React

Połączenie intuicyjności z wydajnością, które zapewnia bezproblemową skalowalność kodu.

SEO & Performance

Audyt techniczny i optymalizacja pod kątem SEO i GEO.

Automatyzacja AI

Bezpieczne automatyzacje procesów i agenci AI w n8n, Make i Claude.

QA & Automation

Testy automatyczne komponentów i E2E w Cypress.

Doradztwo produktowe

Połączenie perspektywy produktu, developera i marketingu w jednym miejscu

RealizacjeO mnieBlog
Porozmawiajmy
PL
EN

Nowoczesne strony internetowe dla firm, które myślą odważnie.

Przewiń do góry

Nazwa

StriveLab Maciej Sala

NIP

6772218995

REGON

524008527

E-mail

contact@strivelab.pl

Usługi główne
  • Tworzenie stron internetowych
  • Strony internetowe Next.js
  • Strony internetowe Astro
  • Strony internetowe React
Inne usługi
  • Usługi
  • SEO & Performance Sprint
  • QA & Stabilizacja
  • Konsultacje Product / Delivery
  • Automatyzacja Procesów AI
  • Aplikacje webowe Next.js
  • Współpraca ciągła
Strony
  • O mnie
  • Usługi
  • Realizacje
  • Blog

© 2026 StriveLab.pl

Polityka prywatności
ReactTypeScriptJavaScript

TypeScript w React bez bólu — 7 wzorców, które realnie robią różnicę

TypeScript w React bez frustracji — ComponentProps, discriminated unions, polymorphic components, generic components, as prop, satisfies, stable types. 7 wzorców z produkcyjnych projektów.

OpublikujLinkedInFacebookWyślij
Autor
Maciej Sala
Opublikowano
24 kwietnia 2026 00:00
Czytanie
5 min czytania
Aktualizacja
31 maja 2026 08:00

W tym artykule pokazuję siedem wzorców, które sam stosuję produkcyjnie — w StriveLab, Army Builder, i u klientów. Każdy z nich rozwiązuje konkretny problem, który w 2026 roku masz rozwiązywać lepiej niż pięć lat temu, bo TypeScript i React dostały nowe narzędzia.

Artykuł w skrócie

  • ComponentProps pozwala nie przepisywać ręcznie natywnych propsów HTML i komponentów zewnętrznych.
  • Discriminated unions to typy wariantów rozróżniane wspólnym polem, np. kind lub variant, co ułatwia bezpieczne modelowanie stanów. dają bezpieczne warianty komponentów bez chaotycznych optional propsów.
  • satisfies sprawdza zgodność wartości z typem, ale zachowuje dokładniejsze typy wywnioskowane z konkretnego obiektu. pomaga walidować obiekty konfiguracyjne bez utraty wąskich typów.
  • Unikaj any i przypadkowego castowania as, bo kasują najważniejszą wartość TypeScriptu.

TypeScript dodaje statyczne typowanie do JavaScriptu, dzięki czemu wiele błędów komponentów łapiesz przed uruchomieniem aplikacji. 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ść developeró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.

Wzorzec 1: ComponentProps zamiast ręcznego definiowania propów

Ręczne wypisywanie propów HTML elementu to najczęstszy błąd początkujących — i najbardziej kosztowny w utrzymaniu:

Code
// ŹLE — ręczne wypisywanie
type ButtonProps = {
  onClick?: (e: React.MouseEvent) => void
  disabled?: boolean
  type?: 'button' | 'submit' | 'reset'
  className?: string
  children?: React.ReactNode
  // ... 50 więcej propów, których nie pamiętasz
}

Zapomniałeś aria-label, tabIndex, autoFocus, form. Za każdym razem. TypeScript ma natywny helper, który to rozwiązuje:

Code
// DOBRZE
import { ComponentProps } from 'react'
 
type ButtonProps = ComponentProps<'button'> & {
  variant?: 'primary' | 'secondary'
}
 
function Button({ variant = 'primary', className, ...rest }: ButtonProps) {
  return (
    <button className={`btn btn-${variant} ${className ?? ''}`} {...rest} />
  )
}
 
// Użycie — wszystkie natywne atrybuty button'a działają
;<Button
  variant="primary"
  onClick={handleClick}
  aria-label="Save"
  disabled={loading}
  type="submit"
/>

ComponentProps<'button'> daje Ci wszystko, co HTML'owy button ma (plus refy, eventy React). Rozszerzasz o swoje customy przez & { ... }.

Dla komponentów zewnętrznych:

Code
import { ComponentProps } from 'react'
import { Link } from 'react-router'
 
type NavLinkProps = ComponentProps<typeof Link> & {
  icon?: React.ReactNode
}

ComponentProps<typeof X> wyciąga propsy dowolnego komponentu.

Wzorzec 2: Discriminated unions dla wariantów komponentu

Gdy komponent ma różne „tryby" — każdy z innymi propsami — klasyczne podejście z optional propsami szybko staje się pułapką:

Code
// ŹLE
type AlertProps = {
  type: 'info' | 'error' | 'success'
  message: string
  errorCode?: string // tylko dla error
  action?: () => void // tylko dla success
}
 
function Alert({ type, message, errorCode, action }: AlertProps) {
  // TypeScript nie wie, że errorCode i action są mutually exclusive
}

User może przekazać type="info" z errorCode — TypeScript się nie poskarży, choć to bez sensu.

Rozwiązanie — discriminated union:

Code
// DOBRZE
type AlertProps =
  | { type: 'info'; message: string }
  | { type: 'error'; message: string; errorCode: string }
  | { type: 'success'; message: string; action?: () => void };
 
function Alert(props: AlertProps) {
  if (props.type === 'error') {
    // TypeScript wie, że errorCode istnieje
    console.log(props.errorCode);
  }
  if (props.type === 'success') {
    // TypeScript wie, że action może istnieć, ale errorCode nie
    props.action?.();
  }
  return <div className={`alert alert-${props.type}`}>{props.message}</div>;
}
 
// Użycie
<Alert type="error" message="Something went wrong" errorCode="ERR_500" />
<Alert type="info" message="New feature available" />
// <Alert type="info" errorCode="..." /> — ERROR: errorCode is not valid for type='info'

TypeScript wymusza, że jeśli type to error, musisz podać errorCode. Jeśli info, errorCode jest niedozwolony. Błędy są łapane w compile time.

Stosuję to regularnie dla Button z loading state:

Code
type ButtonProps =
  | ({ loading?: false } & ComponentProps<'button'>)
  | ({ loading: true; loadingText: string } & ComponentProps<'button'>)

Jeśli loading={true}, musisz podać loadingText. Wymuszone przez typy.

Wzorzec 3: Generic components

Gdy komponent powinien działać z danymi różnych typów — Select, List, Autocomplete — generics dają pełne bezpieczeństwo typów bez powielania kodu:

Code
// DOBRZE
type SelectProps<T> = {
  items: T[]
  getLabel: (item: T) => string
  getValue: (item: T) => string
  value: T | null
  onChange: (value: T) => void
}
 
function Select<T>({
  items,
  getLabel,
  getValue,
  value,
  onChange,
}: SelectProps<T>) {
  return (
    <select
      value={value ? getValue(value) : ''}
      onChange={(e) => {
        const selected = items.find((item) => getValue(item) === e.target.value)
        if (selected) onChange(selected)
      }}
    >
      {items.map((item) => (
        <option key={getValue(item)} value={getValue(item)}>
          {getLabel(item)}
        </option>
      ))}
    </select>
  )
}
 
// Użycie — generyczny User
type User = { id: string; name: string; email: string }
 
;<Select<User>
  items={users}
  getLabel={(user) => user.name}
  getValue={(user) => user.id}
  value={selectedUser}
  onChange={setSelectedUser}
/>

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:

Code
function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    const stored = localStorage.getItem(key)
    return stored ? JSON.parse(stored) : initialValue
  })
 
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value))
  }, [key, value])
 
  return [value, setValue] as const
}
 
// Użycie
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')

Wzorzec 4: Polymorphic component pozwala zmienić renderowany element lub komponent przez prop, często nazywany as. z as prop

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:

Code
import { ElementType, ComponentPropsWithoutRef } from 'react';
 
type ButtonBaseProps = {
  variant?: 'primary' | 'secondary';
  children?: React.ReactNode;
};
 
type ButtonProps<E extends ElementType> = ButtonBaseProps & {
  as?: E;
} & Omit<ComponentPropsWithoutRef<E>, keyof ButtonBaseProps | 'as'>;
 
function Button<E extends ElementType = 'button'>({
  as,
  variant = 'primary',
  children,
  ...rest
}: ButtonProps<E>) {
  const Component = as || 'button';
  return (
    <Component className={`btn btn-${variant}`} {...rest}>
      {children}
    </Component>
  );
}
 
// Użycie
<Button onClick={handleClick}>Click me</Button>  // renderuje <button>
<Button as="a" href="/home">Home</Button>  // renderuje <a>
<Button as={Link} to="/home">Home</Button>  // renderuje <Link>

TypeScript infer'uje propsy na podstawie as — dla as="a" href jest wymagany, dla as="button" nie.

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
// PRZED satisfies
const ROUTES = {
  home: '/',
  about: '/about',
  blog: '/blog',
} as const
 
// Problem: używaj jako string
const path: string = ROUTES.home // OK
 
// Ale też
ROUTES.home.toLowerCase() // OK, bo 'string'
// ale nie widzimy, że 'home' to literalnie '/'

Z satisfies:

Code
const ROUTES = {
  home: '/',
  about: '/about',
  blog: '/blog',
} satisfies Record<string, string>
 
// TypeScript nadal widzi ROUTES.home jako '/' (literal), ale sprawdza typ
type Path = (typeof ROUTES)[keyof typeof ROUTES]
// Path = '/' | '/about' | '/blog'

Różnica: as const mówi „narrow jak najmocniej", satisfies mówi „sprawdź, że to pasuje do typu, ale zachowaj wąskie typy literalne".

Dla typed color palettes, route maps, enum'ów — nieocenione:

Code
const COLORS = {
  primary: '#0066ff',
  secondary: '#ff6600',
  danger: '#ff0000',
} satisfies Record<string, `#${string}`>
 
type ColorName = keyof typeof COLORS // 'primary' | 'secondary' | 'danger'
 
function Button({ color }: { color: ColorName }) {
  return <button style={{ background: COLORS[color] }}>Click</button>
}

Wzorzec 6: Typowanie forwardRef bez bólu

Klasyczny problem — forwardRef mesh'uje się z generics. W React 19 problemu prawie nie ma, bo ref jest zwykłym propem:

Code
// React 19+ — ref jako regular prop
type InputProps = ComponentProps<'input'> & {
  label: string
  ref?: React.Ref<HTMLInputElement>
}
 
function Input({ label, ref, ...rest }: InputProps) {
  return (
    <label>
      <span>{label}</span>
      <input ref={ref} {...rest} />
    </label>
  )
}
 
// Użycie
const inputRef = useRef<HTMLInputElement>(null)
;<Input label="Email" ref={inputRef} type="email" />

Żadnego forwardRef. TypeScript infer'uje wszystko naturalnie. Jeśli nadal używasz forwardRef (React 18), składnia:

Code
// React 18
import { forwardRef, ComponentProps } from 'react'
 
type InputProps = ComponentProps<'input'> & { label: string }
 
const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, ...rest }, ref) => {
    return (
      <label>
        <span>{label}</span>
        <input ref={ref} {...rest} />
      </label>
    )
  },
)

Jeśli masz React 18 w projekcie, migracja na React 19 i usunięcie forwardRef to jedno z najłatwiejszych quick wins dla czytelności kodu. Jeśli jednak publikujesz bibliotekę komponentów, która ma wspierać React 18 i 19 jednocześnie, zostaw forwardRef w publicznym API do czasu, aż świadomie porzucisz React 18.

Notatka

React 19 nie usuwa forwardRef natychmiast. Po prostu sprawia, że w nowym kodzie nie musisz go używać, bo ref możesz przekazać jako zwykły prop.

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 krzyczy
function Input() {
  const handleChange = (e: Event) => {
    setValue(e.target.value) // Error: Property 'value' does not exist on 'EventTarget'
  }
}
 
// DOBRZE — typowane eventy Reactowe
import { 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, które warto znać na skróty

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 children
type Props = {
  children: React.ReactNode // wszystko, co się da wyrenderować
}
 
// Props dla style
type Props = {
  style?: React.CSSProperties // typowany style object
}
 
// Handler dla event'u z komponentu
type Props = {
  onClick: React.MouseEventHandler<HTMLButtonElement>
  onSubmit: React.FormEventHandler<HTMLFormElement>
}
 
// Ref do konkretnego HTML element'u
const divRef = useRef<HTMLDivElement>(null)
 
// Setter dla useState
type SetValueAction<T> = React.Dispatch<React.SetStateAction<T>>
 
// Context z properti'ami
const ThemeContext = createContext<{
  theme: string
  setTheme: (theme: string) => void
}>(null!) // null! bo wiemy, że Provider zawsze ustawi

Anti-patterns do unikania

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 — społeczność odchodzi od tej adnotacji, bo niczego nie daje, a implicitnie dodaje children i utrudnia generic components. Zwykła funkcja z typowanymi propsami jest czytelniejsza i bardziej przewidywalna.

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.

Werdykt Labu

TypeScript w React w 2026 to nie pytanie „czy używać", tylko „jak używać dobrze". Te siedem wzorców pokrywa jakieś 80% sytuacji, w których typy robią się trudniejsze niż zwykłe string | number — ComponentProps zamiast przepisywania propsów, discriminated unions zamiast chaosu optional propsów, satisfies zamiast as. Każdy z nich zamienia całą klasę potencjalnych bugów w błąd kompilacji, czyli łapie je, zanim trafią do użytkownika.

Najwięcej wartości daje przy tym jedna zasada: zawężaj typy, zamiast je rozszerzać. 'small' | 'medium' | 'large' zamiast string, unknown zamiast any, type guard zamiast as. Jeśli prowadzisz projekt React, w którym TypeScript generuje więcej frustracji niż wartości, napisz do mnie — w StriveLab robię audyty z naciskiem na jakość typów i regularnie znajduję miejsca, gdzie lepsza struktura typów eliminuje całe klasy błędów.

  • Wzorzec 1: ComponentProps zamiast ręcznego definiowania propów1 min
  • Wzorzec 2: Discriminated unions dla wariantów komponentu1 min
  • Wzorzec 3: Generic components1 min
  • Wzorzec 4: <Term id="polymorphic-components">Polymorphic components</Term> z as prop1 min
  • Wzorzec 5: satisfies dla const objects1 min
  • Wzorzec 6: Typowanie forwardRef bez bólu1 min
  • Wzorzec 7: Type-safe event handlers i eventy DOM1 min
  • Typy, które warto znać na skróty1 min
  • Anti-patterns do unikania1 min
  • Werdykt Labu1 min

Często zadawane pytania

ŹródłaZweryfikowano: 30 maja 2026

Dokumentacja TypeScript i React zweryfikowana podczas redakcji artykułu.

  • React + TypeScript — typowanie propsów
  • TypeScript — satisfies operator
  • TypeScript — Discriminated unions
  • React TypeScript Cheatsheet

Seria

React w praktyce 2026
Część 4 / 4
  1. 1React 19 Actions — formularz bez onSubmit, useOptimistic i useActionState w praktyce
  2. 2React Compiler w 2026 — czy useMemo i useCallback są już martwe?
  3. 3React Query (TanStack) vs SWR vs useEffect — kompletny przewodnik po fetchingu w 2026
  4. TypeScript w React bez bólu — 7 wzorców, które realnie robią różnicę
Maciej Sala

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 rozwija własne projekty side-projecty.

Moje artykułyWięcej o mnie

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Dlaczego doświadczeni programiści porzucają React na rzecz czystego JS i HTMX
Dlaczego doświadczeni programiści porzucają React na rzecz czystego JS i HTMX

Coraz więcej zespołów wraca do podejścia "web standards first". Zobacz, kiedy vanilla JS i HTMX realnie upraszczają projekt, a kiedy React nadal jest lepszym wyborem.

Maciej Sala

Maciej Sala

Founder Strivelab

4 sierpnia 2025
Prognozy i trendy 2026: AI, GEO i React Server Components
Prognozy i trendy 2026: AI, GEO i React Server Components

Siedem trendów, które w 2026 realnie zmieniają web development i marketing: GEO, zero-click search, server-first Next.js, AI-assisted development i agentic commerce.

Maciej Sala

Maciej Sala

Founder Strivelab

8 stycznia 2026
React 19 Actions — formularz bez onSubmit, useOptimistic i useActionState w praktyce
React 19 Actions — formularz bez onSubmit, useOptimistic i useActionState w praktyce

React 19 Actions eliminują boilerplate z formularzy: useActionState zarządza stanem wysyłki, useOptimistic aktualizuje UI natychmiast, a useFormStatus synchronizuje komponenty dzieci. Praktyczne przykłady i zasady migracji.

Maciej Sala

Maciej Sala

Founder Strivelab

24 kwietnia 2026
Poprzedni wpisAnthropic uderza w Figmę i Adobe — oto Claude DesignAnthropic wypuścił właśnie narzędzie AI do tworzenia stron, landing page'ów i prezentacji z promptu. Oto co wiemy o Claude Design i Opus 4.7 — i co to oznacza dla developerów.
Maciej Sala

Maciej Sala

Founder Strivelab

17 kwietnia 2026
Następny wpisSEO w Astro — Core Web Vitals, dane uporządkowane i techniczny fundament rankingu w 2026Jak zbudować stronę w Astro, która dominuje w SEO — Core Web Vitals, sitemap, robots.txt, metadane, dane uporządkowane i GEO/AEO. Przewodnik techniczny z konkretnymi implementacjami.
Maciej Sala

Maciej Sala

Founder Strivelab

24 kwietnia 2026