Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Udostępnij Udostępnij Kontakt
Wprowadzenie

Przez całą serię używaliśmy TypeScript – typowaliśmy właściwości (props), stan (state), zdarzenia (events), hooki (hooks). Ale dotychczas korzystaliśmy z podstaw: interfejsów, typów prostych, typów unijnych (union types). TypeScript ma o wiele więcej do zaoferowania: typy użytkowe (utility types), typy generyczne (generics), strażnicy typów (type guards), typy warunkowe (conditional types), typy mapowane (mapped types) i więcej.

Te zaawansowane techniki pozwalają na:

  • Pisanie reużywalnych, generycznych komponentów działających z różnymi typami danych
  • Automatyczne generowanie typów z innych typów - eliminacja duplikacji
  • Bezpieczne typowo wywołania API (Type-safe API calls) - błędy wykrywane podczas pisania kodu, nie w runtime
  • Eliminację duplikacji w definicjach typów
  • Lepsze wsparcie IDE i autouzupełnianie (autocomplete)

W tym wpisie przeniesiemy naszą wiedzę TypeScript na wyższy poziom. Poznamy zaawansowane wzorce specyficzne dla React, nauczymy się pisać bezpieczne typowo (type-safe) komponenty i hooki, a także zobaczymy jak wykorzystać pełnię możliwości TypeScript. To będzie techniczny wpis – po nim Twój kod TypeScript będzie na poziomie starszego programisty (senior developer)!

Typy użytkowe w React (Utility Types)

TypeScript ma wbudowane typy użytkowe (utility types) - gotowe narzędzia do transformacji typów. Zamiast pisać skomplikowane definicje typów od zera, używasz gotowych "pomocników". Są niezwykle przydatne w React do manipulowania typami props, state i danych z API.

Partial<T> – wszystkie pola opcjonalne

Partial zamienia wszystkie pola typu na opcjonalne (dodaje ? do każdego pola). Idealne do formularzy edycji gdzie użytkownik może zmienić tylko część danych.

Zastosowania: formularze edycji, draft/szkice obiektów, częściowe aktualizacje danych.

interface User {
  id: number;
  name: string;
  email: string;
  avatar: string;
}

// Komponent do edycji – nie wszystkie pola muszą być wypełnione na start
interface UserFormProps {
  initialValues: Partial<User>; // Wszystkie pola opcjonalne: id?, name?, email?, avatar?
  onSubmit: (values: User) => void;
}

function UserForm({ initialValues, onSubmit }: UserFormProps) {
  const [values, setValues] = useState<Partial<User>>(initialValues);
  
  // values może mieć tylko część pól - TypeScript to rozumie
  console.log(values.name); // string | undefined
  console.log(values.email); // string | undefined
}

Required<T> – wszystkie pola wymagane

Required to odwrotność Partial - zamienia wszystkie opcjonalne pola na wymagane (usuwa ?). Przydatne gdy API wymaga kompletnych danych, ale Twój typ ma opcjonalne pola.

interface UserDraft {
  name?: string;      // Opcjonalne
  email?: string;     // Opcjonalne
  avatar?: string;    // Opcjonalne
}

// API wymaga wszystkich pól - używamy Required
function createUser(user: Required<UserDraft>): Promise<User> {
  // user.name, user.email, user.avatar są WYMAGANE!
  // TypeScript wymusi przekazanie wszystkich pól
  return fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify(user)
  }).then(res => res.json());
}

// ✅ OK - wszystkie pola obecne
createUser({ name: 'Jan', email: 'jan@example.com', avatar: 'avatar.jpg' });

// ❌ Error - brakuje pól
createUser({ name: 'Jan' }); // Error: brak email i avatar!

Pick<T, Keys> – wybierz tylko niektóre pola

Pick tworzy nowy typ zawierający tylko wybrane pola z oryginalnego typu. Używaj gdy komponent potrzebuje tylko części danych z większego obiektu.

Zastosowania: komponenty wyświetlające tylko część danych, API zwracające uproszczone wersje obiektów, bezpieczeństwo - nie przekazuj całego obiektu jeśli nie trzeba.

interface User {
  id: number;
  name: string;
  email: string;
  password: string;    // Wrażliwe dane!
  role: string;
  createdAt: Date;
}

// Komponent pokazujący tylko podstawowe info - bez hasła, roli, daty
type UserBasicInfo = Pick<User, 'id' | 'name' | 'email'>;
// Rezultat: { id: number; name: string; email: string; }

function UserCard({ user }: { user: UserBasicInfo }) {
  return (
    <div>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      {/* user.password nie istnieje w typie - TypeScript nie pozwoli użyć! */}
    </div>
  );
}

Omit<T, Keys> – usuń niektóre pola

Omit to odwrotność Pick - tworzy typ BEZ wybranych pól. Łatwiej użyć gdy chcesz wykluczyć kilka pól niż wybrać wszystkie pozostałe.

Zastosowania: formularze (bez ID generowanego przez backend), API responses (bez wrażliwych danych jak hasła), typy dla frontend (bez pól tylko dla backend).

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// Formularz bez ID (ID generuje backend po zapisie)
type UserFormData = Omit<User, 'id'>;
// Rezultat: { name: string; email: string; password: string; }

function UserCreateForm() {
  const [formData, setFormData] = useState<UserFormData>({
    name: '',
    email: '',
    password: ''
    // id nie istnieje w typie - nie możesz go ustawić!
  });
}

// API response bez hasła (bezpieczeństwo - nie wysyłaj hasła do frontendu!)
type UserPublic = Omit<User, 'password'>;
// Rezultat: { id: number; name: string; email: string; }

function UserProfile({ user }: { user: UserPublic }) {
  // user.password nie istnieje - TypeScript nie pozwoli użyć!
  return <div>{user.name}</div>;
}

Record<Keys, Type> – obiekt z określonymi kluczami

Record tworzy typ obiektu gdzie znasz z góry wszystkie klucze i typ wartości. Idealny do map, słowników, konfiguracji.

Zastosowania: mapy statusów do kolorów, mapy komponentów, słowniki tłumaczeń, konfiguracje.

// Mapy statusów - TypeScript wymusi definicję WSZYSTKICH statusów
type Status = 'pending' | 'approved' | 'rejected';

const statusColors: Record<Status, string> = {
  pending: '#f39c12',      // Musi być
  approved: '#2ecc71',     // Musi być
  rejected: '#e74c3c'      // Musi być
  // TypeScript wymusza wszystkie 3 statusy!
  // Jeśli zapomnisz któregoś - błąd kompilacji!
};

// Mapy komponentów dla routingu
type PageName = 'home' | 'about' | 'contact';

const pages: Record<PageName, React.ComponentType> = {
  home: HomePage,
  about: AboutPage,
  contact: ContactPage
  // Musisz zdefiniować wszystkie 3 strony
};

// Użycie
function Router({ page }: { page: PageName }) {
  const PageComponent = pages[page];  // Zawsze istnieje!
  return <PageComponent />;
}

Readonly<T> – wszystkie pola tylko do odczytu

Readonly uniemożliwia modyfikację pól obiektu. TypeScript wyrzuci błąd jeśli spróbujesz zmienić wartość. Używaj dla konfiguracji, stałych, danych niemutowalnych.

interface Config {
  apiUrl: string;
  timeout: number;
}

const config: Readonly<Config> = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

// ❌ Error: Cannot assign to 'apiUrl' because it is a read-only property
config.apiUrl = 'https://new-api.com';  // TypeScript nie pozwoli!

// ✅ Musisz stworzyć nowy obiekt
const newConfig: Readonly<Config> = {
  ...config,
  apiUrl: 'https://new-api.com'  // Nowy obiekt, nie mutacja!
};

ReturnType<T> – typ zwracany przez funkcję

ReturnType automatycznie wyciąga typ zwracany przez funkcję. Nie musisz ręcznie definiować tego samego typu dwa razy!

Zastosowania: wyciąganie typów z funkcji API, DRY (Don't Repeat Yourself), automatyczna synchronizacja typów.

// Funkcja zwracająca Promise z obiektem użytkownika
function fetchUser(id: number) {
  return fetch(`/api/users/${id}`)
    .then(res => res.json())
    .then(data => ({
      id: data.id,
      name: data.name,
      email: data.email
    }));
}

// Automatycznie wyciąga typ zwracany - nie musisz pisać ręcznie!
type User = Awaited<ReturnType<typeof fetchUser>>;
// User = { id: any; name: any; email: any }

// Teraz możesz użyć typu User w innych miejscach
function UserProfile({ user }: { user: User }) {
  return <div>{user.name}</div>;
}
💡 Awaited<T> to też utility type - wyciąga typ z Promise. Promise<User>User

Parameters<T> – parametry funkcji

Parameters wyciąga typy parametrów funkcji jako tuple (krotkę). Przydatne gdy chcesz przekazać te same parametry do innej funkcji.

function createUser(name: string, email: string, age: number) {
  // Implementacja...
}

// Wyciąga typy parametrów jako tuple
type CreateUserParams = Parameters<typeof createUser>;
// CreateUserParams = [string, string, number]

// Możesz użyć do wrapper funkcji
function createUserWrapper(...args: CreateUserParams) {
  console.log('Creating user with:', args);
  return createUser(...args);  // Przekazanie tych samych parametrów
}

// Użycie
createUserWrapper('Jan', 'jan@example.com', 30);  // ✅ Typy się zgadzają
createUserWrapper('Jan', 30, 'jan@example.com');  // ❌ Error: złe typy!
Typy generyczne w komponentach (Generics)

Typy generyczne (Generics) to funkcje dla typów - pozwalają pisać kod który działa z różnymi typami danych zachowując pełne bezpieczeństwo typów. Zamiast pisać osobny komponent dla listy użytkowników, osobny dla listy produktów itd., piszesz JEDEN generyczny komponent który działa z dowolnym typem!

Korzyści: Mniej duplikacji kodu, reużywalność, pełne bezpieczeństwo typów, lepsze autouzupełnianie w IDE.

Prosty przykład: Generyczna lista

Zacznijmy od klasycznego przykładu - komponent listy który może wyświetlić listę CZEGOKOLWIEK (użytkowników, produktów, zamówień...).

// T to "placeholder" dla typu - może być dowolny typ
interface ListProps<T> {
  items: T[];                                      // Lista elementów typu T
  renderItem: (item: T) => React.ReactNode;       // Funkcja renderująca element typu T
  keyExtractor: (item: T) => string | number;     // Funkcja wyciągająca klucz z elementu T
}

// Komponent generyczny - działa z dowolnym typem T
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map(item => (
        <li key={keyExtractor(item)}>
          {renderItem(item)}
        </li>
      ))}
    </ul>
  );
}

// Użycie z różnymi typami danych
interface User {
  id: number;
  name: string;
}

interface Product {
  id: string;
  title: string;
  price: number;
}

function App() {
  const users: User[] = [
    { id: 1, name: 'Jan' },
    { id: 2, name: 'Anna' }
  ];
  
  const products: Product[] = [
    { id: 'p1', title: 'Laptop', price: 3000 },
    { id: 'p2', title: 'Mouse', price: 50 }
  ];

  return (
    <>
      {/* TypeScript automatycznie wywnioskuje T = User */}
      <List
        items={users}
        renderItem={(user) => <span>{user.name}</span>}  {/* user ma typ User! */}
        keyExtractor={(user) => user.id}                   {/* user.id to number */}
      />

      {/* TypeScript automatycznie wywnioskuje T = Product */}
      <List
        items={products}
        renderItem={(product) => (                         {/* product ma typ Product! */}
          <span>{product.title} - {product.price}zł</span>
        )}
        keyExtractor={(product) => product.id}             {/* product.id to string */}
      />
    </>
  );
}
✨ Magia generics: TypeScript automatycznie wywnioskował typ T na podstawie przekazanej tablicy items! Nie musieliśmy pisać <List<User> items={users} />. TypeScript jest wystarczająco mądry żeby sam to rozgryźć.

Select (dropdown) z generics

Bardziej zaawansowany przykład - generyczny komponent select który może wyświetlić dropdown z dowolnymi obiektami.

interface SelectProps<T> {
  options: T[];                              // Lista opcji typu T
  value: T | null;                           // Wybrana wartość (lub null)
  onChange: (value: T) => void;              // Callback przy zmianie
  getLabel: (option: T) => string;           // Jak wyświetlić opcję
  getValue: (option: T) => string | number;  // Jak wyciągnąć wartość (value dla <option>)
}

function Select<T>({
  options,
  value,
  onChange,
  getLabel,
  getValue
}: SelectProps<T>) {
  return (
    <select
      value={value ? getValue(value) : ''}
      onChange={(e) => {
        const selectedValue = e.target.value;
        // Znajdź opcję która ma tę wartość
        const selectedOption = options.find(
          opt => getValue(opt).toString() === selectedValue
        );
        if (selectedOption) {
          onChange(selectedOption);  // Zwracamy cały obiekt, nie tylko value!
        }
      }}
    >
      {options.map(option => (
        <option key={getValue(option)} value={getValue(option)}>
          {getLabel(option)}
        </option>
      ))}
    </select>
  );
}

// Przykład użycia - wybór kraju
interface Country {
  code: string;
  name: string;
  flag: string;
}

function CountrySelector() {
  const [country, setCountry] = useState<Country | null>(null);
  
  const countries: Country[] = [
    { code: 'PL', name: 'Polska', flag: '🇵🇱' },
    { code: 'US', name: 'USA', flag: '🇺🇸' },
    { code: 'DE', name: 'Niemcy', flag: '🇩🇪' }
  ];

  return (
    <Select
      options={countries}               // T = Country
      value={country}
      onChange={setCountry}             // setCountry otrzyma cały obiekt Country
      getLabel={(c) => `${c.flag} ${c.name}`}  // Wyświetl flagę i nazwę
      getValue={(c) => c.code}                 // Użyj kodu jako value
    />
  );
}

Tabela (Table) z generics

Najbardziej zaawansowany przykład - generyczna tabela z konfigurowalnymi kolumnami. Ten komponent możesz użyć dla DOWOLNYCH danych!

// Definicja kolumny dla typu T
interface Column<T> {
  key: keyof T;                                          // Klucz musi być kluczem z T
  header: string;                                        // Nagłówek kolumny
  render?: (value: T[keyof T], item: T) => React.ReactNode;  // Opcjonalny custom renderer
}

interface TableProps<T> {
  data: T[];                                   // Dane do wyświetlenia
  columns: Column<T>[];                        // Kolumny
  keyExtractor: (item: T) => string | number;  // Klucz dla wiersza
}

function Table<T>({ data, columns, keyExtractor }: TableProps<T>) {
  return (
    <table>
      <thead>
        <tr>
          {columns.map(col => (
            <th key={col.key as string}>{col.header}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map(item => (
          <tr key={keyExtractor(item)}>
            {columns.map(col => (
              <td key={col.key as string}>
                {col.render 
                  ? col.render(item[col.key], item)  // Custom renderer jeśli podany
                  : String(item[col.key])            // Domyślnie toString()
                }
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// Przykład użycia - tabela użytkowników
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
  active: boolean;
}

function UserTable({ users }: { users: User[] }) {
  return (
    <Table
      data={users}
      keyExtractor={(user) => user.id}
      columns={[
        { 
          key: 'name',      // TypeScript sprawdzi czy 'name' istnieje w User!
          header: 'Imię' 
        },
        { 
          key: 'email', 
          header: 'Email' 
        },
        { 
          key: 'role', 
          header: 'Rola',
          render: (role) => (  // role ma typ 'admin' | 'user'
            <span className={role === 'admin' ? 'badge-admin' : 'badge-user'}>
              {role}
            </span>
          )
        },
        {
          key: 'active',
          header: 'Aktywny',
          render: (active) => active ? '✅' : '❌'  // active ma typ boolean
        }
      ]}
    />
  );
}
🔍 Zauważ: TypeScript sprawdza czy key w Column to rzeczywiście klucz z User! Jeśli napiszesz key: 'firstName' (nie ma takiego pola), dostaniesz błąd kompilacji. To właśnie potęga generics + keyof!
Strażnicy typów (Type Guards)

Strażnicy typów (Type Guards) to funkcje lub wyrażenia, które pozwalają TypeScript zawęzić typ zmiennej w określonym bloku kodu. W JavaScript zmienne mogą mieć różne typy w runtime - Type Guards pomagają TypeScript zrozumieć jaki typ ma zmienna W DANYM MOMENCIE wykonania kodu.

Problem bez type guards: Masz zmienną która może być stringiem LUB numberem. TypeScript nie pozwoli Ci użyć metod specyficznych dla string (np. toUpperCase()) dopóki nie upewnisz się że to rzeczywiście string.

Rozwiązanie: Type guards sprawdzają typ w runtime i informują TypeScript o wyniku - wtedy TypeScript wie że w danym bloku kodu zmienna MA KONKRETNY TYP.

typeof - strażnik dla typów prymitywnych

typeof to najprostszy type guard - sprawdza typ prymitywny (string, number, boolean). Używasz operatora typeof z JavaScript, a TypeScript automatycznie rozumie co się dzieje.

function formatValue(value: string | number) {
  // value może być string LUB number - TypeScript nie wie który
  
  if (typeof value === 'string') {
    // ✅ W tym bloku TypeScript WIE że value to string
    return value.toUpperCase();  // Metody string działają!
  } else {
    // ✅ W tym bloku TypeScript WIE że value to number
    return value.toFixed(2);     // Metody number działają!
  }
  
  // ❌ Tutaj (poza if/else) TypeScript nadal nie wie który typ
}

// Użycie
formatValue("hello");  // "HELLO"
formatValue(123.456);  // "123.46"

instanceof - strażnik dla klas

instanceof sprawdza czy obiekt jest instancją danej klasy. Idealny do obsługi błędów (Error classes) i klas domenowych.

Zastosowania: Obsługa różnych typów błędów, różne typy eventów, klasy domenowe (User, Admin, Guest).

// Własna klasa błędu z dodatkowym polem statusCode
class ApiError extends Error {
  statusCode: number;
  
  constructor(message: string, statusCode: number) {
    super(message);
    this.statusCode = statusCode;
  }
}

function handleError(error: unknown) {  // unknown = nie wiemy co to jest
  if (error instanceof ApiError) {
    // ✅ TypeScript WIE że error to ApiError
    console.log(`API Error ${error.statusCode}: ${error.message}`);
    // error.statusCode i error.message dostępne!
  } else if (error instanceof Error) {
    // ✅ TypeScript WIE że error to Error (ale nie ApiError)
    console.log(`Error: ${error.message}`);
    // error.message dostępne, ale error.statusCode NIE!
  } else {
    // ❌ TypeScript nie wie co to jest - może być string, number, cokolwiek
    console.log('Unknown error:', error);
  }
}

// Użycie
try {
  throw new ApiError('Not found', 404);
} catch (error) {
  handleError(error);  // "API Error 404: Not found"
}

in - sprawdź czy pole istnieje w obiekcie

Operator in sprawdza czy obiekt ma określone pole. TypeScript używa tego do rozróżnienia typów które mają różne pola.

Zastosowania: Rozróżnianie podobnych interfejsów, API responses z różnymi polami, polimorficzne komponenty.

interface User {
  type: 'user';
  name: string;
  email: string;
}

interface Admin {
  type: 'admin';
  name: string;
  email: string;
  permissions: string[];  // Tylko Admin ma to pole!
}

type Person = User | Admin;

function greet(person: Person) {
  // Sprawdź czy obiekt ma pole 'permissions'
  if ('permissions' in person) {
    // ✅ TypeScript WIE że person to Admin
    console.log(`Admin ${person.name} with ${person.permissions.length} permissions`);
    // person.permissions dostępne!
  } else {
    // ✅ TypeScript WIE że person to User
    console.log(`User ${person.name}`);
    // person.permissions NIE istnieje - TypeScript nie pozwoli użyć
  }
}

// Użycie
const user: User = { type: 'user', name: 'Jan', email: 'jan@example.com' };
const admin: Admin = { 
  type: 'admin', 
  name: 'Anna', 
  email: 'anna@example.com', 
  permissions: ['read', 'write', 'delete'] 
};

greet(user);   // "User Jan"
greet(admin);  // "Admin Anna with 3 permissions"

Własny strażnik typów (Custom type guard)

Możesz stworzyć własne funkcje type guard używając składni argument is Type. TypeScript zaufa Twojej funkcji i zawęzi typ na podstawie jej wyniku.

Kiedy używać: Złożona logika sprawdzania typu, wielokrotne użycie tego samego sprawdzenia, czytelniejszy kod.

interface Cat {
  type: 'cat';
  meow: () => void;
}

interface Dog {
  type: 'dog';
  bark: () => void;
}

type Animal = Cat | Dog;

// Własny type guard - zwraca 'animal is Cat'
function isCat(animal: Animal): animal is Cat {
  return animal.type === 'cat';  // Logika sprawdzenia
}

function makeSound(animal: Animal) {
  if (isCat(animal)) {
    // ✅ TypeScript WIE że animal to Cat
    animal.meow();  // Możemy wywołać meow()
  } else {
    // ✅ TypeScript WIE że animal to Dog
    animal.bark();  // Możemy wywołać bark()
  }
}

// Użycie
const cat: Cat = { 
  type: 'cat', 
  meow: () => console.log('Miau!') 
};

const dog: Dog = { 
  type: 'dog', 
  bark: () => console.log('Hau!') 
};

makeSound(cat);  // "Miau!"
makeSound(dog);  // "Hau!"

Type guard dla null/undefined

Przydatna funkcja pomocnicza do filtrowania null/undefined z tablic. TypeScript wymaga takiej funkcji żeby zrozumieć że po filtrowaniu mamy tablicę bez null/undefined.

// Generyczny type guard - działa dla dowolnego typu T
function isNotNull<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

// Przykład użycia
const values: (string | null)[] = ['hello', null, 'world', null, 'typescript'];

// ❌ Bez type guard - TypeScript myśli że nadal może być null
const filtered1 = values.filter(v => v !== null);  
// Typ: (string | null)[]  - TypeScript nie wie że usunęliśmy null!

// ✅ Z type guard - TypeScript wie że nie ma null
const filtered2 = values.filter(isNotNull);
// Typ: string[]  - TypeScript wie że to tylko stringi!

// Możesz bezpiecznie użyć metod string
filtered2.forEach(str => {
  console.log(str.toUpperCase());  // ✅ Działa - str to zawsze string
});
💡 Pro tip: Type guards to fundament bezpiecznego kodu TypeScript. Używaj ich zawsze gdy pracujesz z union types lub unknown/any. TypeScript jest tylko tak mądry jak Ty mu powiesz!
Rozróżniające unie (Discriminated Unions)

Discriminated Unions (znane też jako Tagged Unions) to zaawansowany wzorzec łączący union types z polem "dyskryminatorem" (znacznikiem). To pole pozwala TypeScript automatycznie rozróżnić który typ z unii masz w danym momencie.

Jak działa: Każdy typ w unii ma wspólne pole (np. type lub status) z unikalną wartością literalną. TypeScript sprawdza to pole i wie dokładnie który typ masz.

Dlaczego potężny wzorzec: Eliminuje błędy w runtime, wymusza obsługę wszystkich przypadków, idealne do state management i API responses.

Typy akcji w reducerze (Action types)

Klasyczne zastosowanie - akcje w Redux/useReducer. Każda akcja ma pole type i opcjonalnie payload z danymi.

// Stan aplikacji
interface State {
  data: User[] | null;
  loading: boolean;
  error: string | null;
}

// Discriminated union - pole 'type' to dyskryminator
type Action =
  | { type: 'FETCH_START' }                        // Brak payload
  | { type: 'FETCH_SUCCESS'; payload: User[] }     // Payload to User[]
  | { type: 'FETCH_ERROR'; payload: string };      // Payload to string

function reducer(state: State, action: Action): State {
  // Switch po polu 'type' - TypeScript wie który typ action mamy!
  switch (action.type) {
    case 'FETCH_START':
      // ✅ TypeScript WIE że to { type: 'FETCH_START' }
      // action.payload NIE istnieje - TypeScript nie pozwoli użyć
      return { ...state, loading: true, error: null };
    
    case 'FETCH_SUCCESS':
      // ✅ TypeScript WIE że action.payload to User[]
      return { data: action.payload, loading: false, error: null };
    
    case 'FETCH_ERROR':
      // ✅ TypeScript WIE że action.payload to string
      return { ...state, loading: false, error: action.payload };
    
    default:
      // Exhaustiveness check - TypeScript sprawdzi czy obsłużyliśmy wszystkie case'y
      const _exhaustive: never = action;
      return state;
  }
}

// Użycie
const [state, dispatch] = useReducer(reducer, {
  data: null,
  loading: false,
  error: null
});

// TypeScript wymusza poprawne typy!
dispatch({ type: 'FETCH_START' });                    // ✅ OK
dispatch({ type: 'FETCH_SUCCESS', payload: users });  // ✅ OK
dispatch({ type: 'FETCH_ERROR', payload: 'Error!' }); // ✅ OK
dispatch({ type: 'FETCH_SUCCESS', payload: 'wrong' }); // ❌ Error - payload musi być User[]!
🔍 Exhaustiveness checking: Linia const _exhaustive: never = action; to sprytny trik! Jeśli zapomnimy obsłużyć jakiś case, TypeScript wyrzuci błąd bo action nie będzie typu never. To wymusza obsługę WSZYSTKICH możliwych akcji!

Typy odpowiedzi API (API Response types)

Discriminated unions są idealne do reprezentowania różnych stanów asynchronicznych operacji. Pole status mówi czy dane się ładują, czy są gotowe, czy wystąpił błąd.

// Generyczna odpowiedź API - może być w 3 stanach
type ApiResponse<T> =
  | { status: 'loading' }                    // Ładowanie - brak data/error
  | { status: 'success'; data: T }           // Sukces - mamy data
  | { status: 'error'; error: string };      // Błąd - mamy error

function UserProfile({ userId }: { userId: number }) {
  const [response, setResponse] = useState<ApiResponse<User>>({ 
    status: 'loading' 
  });

  useEffect(() => {
    // Rozpocznij ładowanie
    setResponse({ status: 'loading' });
    
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setResponse({ status: 'success', data }))  // Sukces
      .catch(err => setResponse({ status: 'error', error: err.message }));  // Błąd
  }, [userId]);

  // Type-safe rendering - TypeScript wymusza obsługę wszystkich stanów
  switch (response.status) {
    case 'loading':
      // ✅ TypeScript WIE że response to { status: 'loading' }
      // response.data NIE istnieje
      return <div>Ładowanie...</div>;
    
    case 'success':
      // ✅ TypeScript WIE że response.data istnieje i ma typ User
      return (
        <div>
          <h1>{response.data.name}</h1>
          <p>{response.data.email}</p>
        </div>
      );
    
    case 'error':
      // ✅ TypeScript WIE że response.error istnieje i jest stringiem
      return <div>Błąd: {response.error}</div>;
  }
}

// ❌ TypeScript wyrzuci błąd jeśli zapomnimy obsłużyć któryś case!
✨ Zaleta: Nie możesz mieć jednocześnie loading: true i data. Stan jest zawsze spójny - albo ładujesz, albo masz dane, albo masz błąd. Żadnych dziwnych kombinacji!
Typy warunkowe (Conditional Types)

Typy warunkowe (Conditional Types) to "if'y dla typów". Pozwalają tworzyć typy które zmieniają się w zależności od warunków. Składnia: T extends U ? X : Y - jeśli T pasuje do U, zwróć X, w przeciwnym razie Y.

Zastosowania: Dynamiczne generowanie typów, wyciąganie typów z innych typów, zaawansowane transformacje.

Podstawy - sprawdzanie typu

// Prosty przykład - sprawdź czy T to string
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<'hello'>; // true (literal string też jest stringiem)

Praktyczny przykład: Wyciąganie typów props z komponentu

Conditional types + infer pozwalają wyciągnąć typy props z komponentu. Nie musisz ręcznie exportować typu props!

// Wyciąga typ props z React komponentu
type ComponentProps<T> = T extends React.ComponentType<infer P> 
  ? P      // Jeśli T to komponent, zwróć jego props (P)
  : never; // W przeciwnym razie never

// Komponent Button
function Button(props: { label: string; onClick: () => void }) {
  return <button onClick={props.onClick}>{props.label}</button>;
}

// Automatycznie wyciąga typy props z Button
type ButtonProps = ComponentProps<typeof Button>;
// ButtonProps = { label: string; onClick: () => void }

// Możesz użyć tego typu w innych miejscach
const buttonProps: ButtonProps = {
  label: 'Kliknij',
  onClick: () => console.log('Kliknięto!')
};
🔍 infer keyword: infer P to "zmienna" dla typu. TypeScript automatycznie wywnioskuje (infer) typ P i możemy go użyć w części "true" warunku.

Zamiana null na undefined w typie

// Zamienia null na undefined w każdym polu typu T
type NullableToOptional<T> = {
  [K in keyof T]: null extends T[K]   // Czy pole może być null?
    ? T[K] | undefined                 // Jeśli tak, dodaj | undefined
    : T[K];                            // Jeśli nie, zostaw jak jest
};

interface User {
  id: number;
  name: string;
  email: string | null;    // Może być null
  phone: string | null;    // Może być null
}

type UserOptional = NullableToOptional<User>;
// {
//   id: number;
//   name: string;
//   email: string | null | undefined;  // Dodano undefined
//   phone: string | null | undefined;  // Dodano undefined
// }
Typy mapowane (Mapped Types)

Typy mapowane (Mapped Types) to sposób na automatyczne transformowanie każdego pola w typie. Używasz pętli [K in keyof T] żeby przejść przez wszystkie klucze typu T i zmienić je według potrzeb.

Zastosowania: Readonly wersje typów, dodawanie prefiksów/suffixów do kluczy, generowanie event handlerów, transformacje na całym typie.

Readonly na wszystkich poziomach (deep)

Wbudowany Readonly<T> działa tylko na pierwszym poziomie. Możemy stworzyć rekurencyjną wersję która zadziała na zagnieżdżonych obiektach.

// Rekurencyjnie readonly - działa na zagnieżdżonych obiektach
type ReadonlyDeep<T> = {
  readonly [K in keyof T]: T[K] extends object  // Czy pole to obiekt?
    ? ReadonlyDeep<T[K]>   // Jeśli tak, zastosuj ReadonlyDeep rekurencyjnie
    : T[K];                // Jeśli nie, zostaw jako readonly
};

interface Config {
  api: {
    url: string;
    timeout: number;
  };
  features: {
    auth: boolean;
    payments: boolean;
  };
}

const config: ReadonlyDeep<Config> = {
  api: { url: 'https://api.com', timeout: 5000 },
  features: { auth: true, payments: false }
};

// ❌ Wszystko readonly, nawet zagnieżdżone obiekty!
config.api.url = 'new';           // Error: readonly!
config.features.auth = false;     // Error: readonly!
config.api = { ... };             // Error: readonly!

Dodawanie prefiksu do kluczy

Mapped types + template literal types pozwalają zmienić nazwy kluczy. Przydatne do generowania typów z prefiksami/suffixami.

// Dodaje prefiks do wszystkich kluczy
type Prefixed<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K];
  // 'as' pozwala zmienić nazwę klucza
  // `${P}${string & K}` to template literal - łączy P z nazwą klucza K
};

interface User {
  name: string;
  email: string;
  age: number;
}

type PrefixedUser = Prefixed<User, 'user'>;
// {
//   username: string;
//   useremail: string;
//   userage: number;
// }

// Użycie
const prefixedUser: PrefixedUser = {
  username: 'Jan',
  useremail: 'jan@example.com',
  userage: 30
};

Generowanie event handlerów

Praktyczny przykład - automatyczne generowanie typów dla event handlerów formularza. Dla każdego pola w FormData, tworzysz onChange handler.

// Generuje event handlery z prefiksem 'on' i kapitalizacją
type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void;
  // Capitalize to wbudowany utility type - 'name' -> 'Name'
};

interface FormData {
  name: string;
  email: string;
  age: number;
  agreed: boolean;
}

type FormHandlers = EventHandlers<FormData>;
// {
//   onName: (value: string) => void;
//   onEmail: (value: string) => void;
//   onAge: (value: number) => void;
//   onAgreed: (value: boolean) => void;
// }

// Użycie w komponencie
function Form({ handlers }: { handlers: FormHandlers }) {
  return (
    <div>
      <input 
        type="text" 
        onChange={(e) => handlers.onName(e.target.value)}  // value: string
      />
      <input 
        type="email" 
        onChange={(e) => handlers.onEmail(e.target.value)}  // value: string
      />
      <input 
        type="number" 
        onChange={(e) => handlers.onAge(Number(e.target.value))}  // value: number
      />
      <input 
        type="checkbox" 
        onChange={(e) => handlers.onAgreed(e.target.checked)}  // value: boolean
      />
    </div>
  );
}
💡 DRY principle: Zdefiniowałeś FormData raz, a EventHandlers wygenerował się automatycznie! Dodasz nowe pole do FormData? Event handler też się doda. Zero duplikacji!
Praktyczny przykład: Bezpieczny typowo klient API (Type-safe API client)

To jest "boss fight" tego wpisu - wykorzystamy WSZYSTKIE poznane techniki do stworzenia w pełni bezpiecznego typowo klienta API. Każde wywołanie API będzie sprawdzane przez TypeScript - złe parametry, brak wymaganych pól, nieprawidłowe typy - wszystko wykryte PRZED uruchomieniem aplikacji!

Co osiągniemy: TypeScript będzie wiedział jakie endpointy istnieją, jakie metody HTTP są dozwolone dla każdego endpointu, jakie parametry są wymagane (params, query, body) i jaki typ zwróci response. Zero any, pełne autouzupełnianie!

Definicja endpointów API

Najpierw definiujemy WSZYSTKIE endpointy naszego API w jednym miejscu. To źródło prawdy dla całej aplikacji.

// Interfejs opisujący WSZYSTKIE endpointy API
interface ApiEndpoints {
  '/users': {
    GET: {
      response: User[];  // GET /users zwraca listę użytkowników
    };
    POST: {
      body: Omit<User, 'id'>;  // POST /users wymaga User bez id (id generuje backend)
      response: User;           // Zwraca utworzonego użytkownika z id
    };
  };
  '/users/:id': {
    GET: {
      params: { id: number };  // GET /users/123 wymaga parametru id
      response: User;          // Zwraca jednego użytkownika
    };
    PUT: {
      params: { id: number };        // PUT /users/123 wymaga id w URL
      body: Partial<User>;           // Body z częściowymi danymi do aktualizacji
      response: User;                // Zwraca zaktualizowanego użytkownika
    };
    DELETE: {
      params: { id: number };        // DELETE /users/123
      response: { success: boolean }; // Zwraca status operacji
    };
  };
  '/posts': {
    GET: {
      query: { userId?: number; limit?: number };  // GET /posts?userId=1&limit=10
      response: Post[];
    };
  };
}

Implementacja klienta API

Teraz magia - klient API który używa tych definicji do wymuszenia bezpieczeństwa typów.

// Type-safe API client z pełnym wsparciem TypeScript
class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  // Generyczna metoda request z pełnym type safety
  async request<
    Path extends keyof ApiEndpoints,              // Path musi być kluczem z ApiEndpoints
    Method extends keyof ApiEndpoints[Path]       // Method musi być kluczem z tego endpointu
  >(
    method: Method,
    path: Path,
    // Options są opcjonalne TYLKO jeśli endpoint nie wymaga body/params/query
    options?: ApiEndpoints[Path][Method] extends { body: infer B }
      ? { body: B }
      : ApiEndpoints[Path][Method] extends { params: infer P }
      ? { params: P }
      : ApiEndpoints[Path][Method] extends { query: infer Q }
      ? { query: Q }
      : never
  ): Promise<
    // Typ response wyciągnięty z definicji endpointu
    ApiEndpoints[Path][Method] extends { response: infer R } ? R : never
  > {
    // Buduj URL z params i query
    const url = this.buildUrl(path as string, options);
    
    // Wykonaj request
    const response = await fetch(url, {
      method: method as string,
      headers: { 'Content-Type': 'application/json' },
      body: options && 'body' in options ? JSON.stringify(options.body) : undefined
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return response.json();
  }

  private buildUrl(path: string, options?: any): string {
    let url = this.baseUrl + path;
    
    // Zamień :param w URL na wartości z options.params
    if (options?.params) {
      Object.entries(options.params).forEach(([key, value]) => {
        url = url.replace(`:${key}`, String(value));
      });
    }
    
    // Dodaj query params
    if (options?.query) {
      const queryString = new URLSearchParams(
        Object.entries(options.query)
          .filter(([_, v]) => v !== undefined)  // Usuń undefined
          .map(([k, v]) => [k, String(v)])
      ).toString();
      if (queryString) {
        url += `?${queryString}`;
      }
    }
    
    return url;
  }
}

Użycie - pełen type safety!

const api = new ApiClient('https://api.example.com');

// ✅ GET /users - brak dodatkowych parametrów
const users = await api.request('GET', '/users');
// users ma typ User[]
console.log(users[0].name);  // TypeScript wie że User ma pole name

// ✅ POST /users - wymaga body typu Omit<User, 'id'>
const newUser = await api.request('POST', '/users', {
  body: { 
    name: 'Jan', 
    email: 'jan@example.com' 
    // id nie może być tutaj - TypeScript wyrzuci błąd!
  }
});
// newUser ma typ User (z id od backendu)

// ✅ GET /users/:id - wymaga params z id
const user = await api.request('GET', '/users/:id', {
  params: { id: 123 }  // TypeScript wymusza obiekt z polem id: number
});
// user ma typ User

// ❌ Błędy wykrywane przez TypeScript:

// Error: Brak wymaganego params
await api.request('GET', '/users/:id');

// Error: Zły typ params (string zamiast number)
await api.request('GET', '/users/:id', {
  params: { id: '123' }
});

// Error: Nieistniejący endpoint
await api.request('GET', '/fake-endpoint');

// Error: Nieobsługiwana metoda dla tego endpointu
await api.request('PATCH', '/users/:id');

// ✅ GET /posts z query params (opcjonalne)
const posts = await api.request('GET', '/posts', {
  query: { userId: 1, limit: 10 }
});
// posts ma typ Post[]

// ✅ Query params są opcjonalne - można pominąć
const allPosts = await api.request('GET', '/posts');
// Też działa!
🎉 Co osiągnęliśmy:
  • Każde wywołanie API sprawdzane przez TypeScript
  • Autouzupełnianie dla endpointów, metod, parametrów
  • Niemożliwe przekazanie złych typów
  • Typ response automatycznie wnioskowany
  • Jedna zmiana w ApiEndpoints = aktualizacja w całej aplikacji
  • Zero any, zero błędów w runtime związanych z API
Zaawansowane wzorce

Ograniczenia w generics (Extends constraints)

Extends constraints ograniczają typ generyczny T do typów spełniających określone warunki. Używaj gdy funkcja generyczna potrzebuje pewnych pól/metod na typie T.

Przykład zastosowania: Funkcja findById działająca dla dowolnego typu, ale TYLKO jeśli ten typ ma pole id.

// Interface określający constraint - musi mieć pole id
interface WithId {
  id: number | string;
}

// T musi rozszerzać WithId - czyli musi mieć pole id!
function findById<T extends WithId>(items: T[], id: T['id']): T | undefined {
  return items.find(item => item.id === id);
  // TypeScript wie że item.id istnieje bo T extends WithId
}

// ✅ Działa - User ma pole id
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: 'Jan' },
  { id: 2, name: 'Anna' }
];

const user = findById(users, 1);  // ✅ OK
// user ma typ User | undefined

// ❌ Error - string[] nie ma pola id
const names: string[] = ['Jan', 'Anna'];
findById(names, 'Jan');  // ❌ Error: string nie rozszerza WithId!

Infer w conditional types - wyciąganie typów

Infer to keyword pozwalający "wyciągnąć" część typu wewnątrz conditional type. TypeScript sam wywnioskuje (infer) ten typ.

Zastosowania: Wyciąganie typu elementu z tablicy, typu z Promise, parametrów funkcji.

// Wyciąga typ elementu z tablicy
type ArrayElement<T> = T extends (infer E)[]  // Jeśli T to tablica E
  ? E       // Zwróć E (typ elementu)
  : never;  // W przeciwnym razie never

type Numbers = ArrayElement<number[]>;  // number
type Strings = ArrayElement<string[]>;  // string
type Users = ArrayElement<User[]>;      // User

// Wyciąga typ z Promise (podobnie jak wbudowany Awaited)
type UnwrapPromise<T> = T extends Promise<infer U>  // Jeśli T to Promise<U>
  ? U       // Zwróć U (typ wewnątrz Promise)
  : T;      // W przeciwnym razie zwróć T

type User = UnwrapPromise<Promise<{ name: string }>>;  // { name: string }
type Num = UnwrapPromise<number>;                      // number (nie Promise)

// Wyciąga typ pierwszego parametru funkcji
type FirstParameter<T> = T extends (first: infer F, ...args: any[]) => any
  ? F
  : never;

function greet(name: string, age: number) {
  return `Hello ${name}, you are ${age}`;
}

type GreetFirstParam = FirstParameter<typeof greet>;  // string

Template Literal Types - manipulacja stringami na poziomie typów

Template Literal Types pozwalają tworzyć nowe typy przez łączenie stringów. Działają jak template strings w JavaScript, ale na poziomie TYPÓW!

Zastosowania: Generowanie typów event names, route patterns, nazw CSS classes.

// Wszystkie kombinacje metod HTTP i ścieżek
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Route = '/users' | '/posts' | '/comments';

type Endpoint = `${HTTPMethod} ${Route}`;
// 'GET /users' | 'GET /posts' | 'GET /comments' | 
// 'POST /users' | 'POST /posts' | ... (16 kombinacji!)

// Generowanie nazw eventów z prefiksem
type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<'click'>;      // 'onClick'
type ChangeEvent = EventName<'change'>;    // 'onChange'
type SubmitEvent = EventName<'submit'>;    // 'onSubmit'

// Praktyczne zastosowanie - CSS classes z prefiksem/suffixem
type Size = 'small' | 'medium' | 'large';
type ButtonClass = `btn-${Size}`;
// 'btn-small' | 'btn-medium' | 'btn-large'

// Użycie w komponencie
function Button({ size }: { size: Size }) {
  const className: ButtonClass = `btn-${size}`;  // TypeScript sprawdzi!
  return <button className={className}>Click</button>;
}
💡 Kombinatoryka typów: Template literal types automatycznie tworzą WSZYSTKIE kombinacje! 4 metody HTTP × 3 ścieżki = 12 typów. Nie musisz ich ręcznie wypisywać!
Najlepsze praktyki (Best Practices)

1. Używaj type inference (wnioskowania typów) gdy możliwe

TypeScript jest mądry - potrafi sam wywnioskować typy. Nie duplikuj informacji jeśli TypeScript może to zrobić za Ciebie.

// ❌ Redundantne (zbędne) typowanie - powtórzenie User[] dwa razy
const [users, setUsers]: [User[], React.Dispatch<...>] = useState<User[]>([]);

// ✅ TypeScript wywnioskuje typ setUsers sam
const [users, setUsers] = useState<User[]>([]);

// ❌ Redundantne
const name: string = 'Jan';

// ✅ TypeScript wie że to string
const name = 'Jan';

// ✅ Typuj TYLKO gdy TypeScript nie może wywnioskować
const users = useState<User[]>([]);  // Tutaj trzeba, bo [] to any[]

2. Unikaj any, używaj unknown

any wyłącza sprawdzanie typów - to dziura w bezpieczeństwie. unknown jest bezpieczniejszą alternatywą - wymusza sprawdzenie typu przed użyciem.

// ❌ any wyłącza type checking - możesz zrobić wszystko
function processAny(data: any) {
  return data.value;  // Brak sprawdzenia - może crashnąć w runtime!
  data.anything();    // TypeScript nie zgłosi błędu
}

// ✅ unknown wymusza type guard
function processUnknown(data: unknown) {
  // ❌ Error: Object is of type 'unknown'
  // return data.value;
  
  // ✅ Musisz sprawdzić typ przed użyciem
  if (typeof data === 'object' && data !== null && 'value' in data) {
    return (data as { value: string }).value;  // Bezpieczne!
  }
  
  throw new Error('Invalid data');
}

// Jeszcze lepiej - użyj type guard
function isValidData(data: unknown): data is { value: string } {
  return typeof data === 'object' && data !== null && 'value' in data;
}

function processBest(data: unknown) {
  if (isValidData(data)) {
    return data.value;  // TypeScript wie że data ma pole value!
  }
  throw new Error('Invalid data');
}

3. Definiuj typy w osobnych plikach

Duże projekty potrzebują organizacji. Typy w osobnych plikach = łatwiejsze znalezienie, reużywalność, czytelność.

// types/user.ts - typy związane z użytkownikami
export interface User {
  id: number;
  name: string;
  email: string;
}

export type UserRole = 'admin' | 'user' | 'guest';

export interface UserWithRole extends User {
  role: UserRole;
}

// types/api.ts - typy dla API
export interface ApiResponse<T> {
  data: T;
  status: number;
  message?: string;
}

export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  page: number;
  totalPages: number;
}

// types/index.ts - re-export wszystkich typów
export * from './user';
export * from './api';

// Użycie w komponencie
import type { User, ApiResponse } from '@/types';

4. Używaj const assertions dla stałych

Const assertions (as const) mówią TypeScript żeby typ był jak najbardziej wąski (literal types zamiast string/number).

// ❌ Typ: string[] - zbyt szeroki
const colors = ['red', 'green', 'blue'];
type Color = typeof colors[number];  // string (za szeroki!)

// ✅ Typ: readonly ['red', 'green', 'blue'] - dokładny!
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number];  // 'red' | 'green' | 'blue' (idealnie!)

// Praktyczne zastosowanie
const ROUTES = {
  home: '/',
  about: '/about',
  contact: '/contact'
} as const;

type Route = typeof ROUTES[keyof typeof ROUTES];  // '/' | '/about' | '/contact'

// Użycie
function navigate(route: Route) {
  // TypeScript wymusi użycie tylko tych 3 ścieżek
}

navigate(ROUTES.home);     // ✅ OK
navigate('/products');     // ❌ Error!

5. Dokumentuj złożone typy

Zaawansowane typy mogą być trudne do zrozumienia. Dodaj komentarze JSDoc - pomogą Tobie i zespołowi, a IDE je wyświetli!

/**
 * Reprezentuje odpowiedź API z paginacją.
 * Używaj tego typu dla wszystkich endpointów zwracających listy z podziałem na strony.
 * 
 * @template T - Typ elementów w tablicy data
 * 
 * @example
 * ```typescript
 * const response: PaginatedResponse<User> = await api.getUsers({ page: 1 });
 * console.log(response.data); // User[]
 * console.log(response.totalPages); // number
 * ```
 */
interface PaginatedResponse<T> {
  /** Tablica elementów na aktualnej stronie */
  data: T[];
  
  /** Numer aktualnej strony (1-indexed, pierwsza strona to 1) */
  page: number;
  
  /** Liczba elementów na jednej stronie */
  perPage: number;
  
  /** Całkowita liczba elementów we wszystkich stronach */
  total: number;
  
  /** Całkowita liczba stron */
  totalPages: number;
}

// IDE wyświetli pełną dokumentację przy użyciu typu!
const response: PaginatedResponse<User> = /* ... */;
Podsumowanie

To był techniczny, ale bardzo wartościowy wpis! Nauczyliśmy się:

  • Typy użytkowe (Utility Types)Partial, Required, Pick, Omit, Record, Readonly, ReturnType, Parameters
  • Typy generyczne w komponentach (Generics) – reużywalne komponenty dla różnych typów (List, Select, Table)
  • Strażnicy typów (Type Guards)typeof, instanceof, in, własne strażniki
  • Rozróżniające unie (Discriminated Unions)bezpieczne typowo akcje (type-safe actions) i odpowiedzi (responses)
  • Typy warunkowe (Conditional Types) – typy zależne od warunków, wyciąganie typów
  • Typy mapowane (Mapped Types) – automatyczna transformacja typów (Readonly, prefiksy, event handlers)
  • Bezpieczny typowo klient API (Type-safe API client) – kompletny przykład wykorzystujący wszystko
  • Zaawansowane wzorceextends constraints, infer, template literals
  • Najlepsze praktyki (Best Practices) – 5 złotych zasad dla czystego kodu

Zaawansowany TypeScript to super moc – pozwala pisać bezpieczniejszy, bardziej skalowalny kod z lepszym doświadczeniem programisty (DX - Developer Experience).

W kolejnym wpisie poznamy React Hook Form – najlepszą bibliotekę do formularzy w React. Nauczymy się walidacji, obsługi błędów (error handling), integracji z TypeScript!

🎯 Zadanie dla Ciebie

  1. Stwórz generyczny komponent DataTable<T> z sortowaniem i filtrowaniem - użyj generics i keyof
  2. Zaimplementuj bezpieczny typowo klient API (type-safe API client) dla swojego backendu - zdefiniuj wszystkie endpointy
  3. Użyj discriminated unions dla zarządzania stanem (state management) w reducerze - każda akcja z type
  4. (Bonus) Stwórz utility type DeepPartial<T> dla zagnieżdżonych obiektów (nested objects) - rekurencyjnie Partial