TypeScript ma bogaty system typów, ale nie musisz rozumieć go w całości, aby być produktywnym. Rzeczywistość jest taka, że 90% pracy wykonuje garść funkcji, po które sięgasz każdego dnia. To jest praktyczna referencja — typy prymitywne, unie, generyki, typy narzędziowe i wzorce zawężania, które sprawiają, że wszystko klika. Żadnej abstrakcyjnej teorii; każdy przykład to kod, który piszesz w prawdziwym projekcie. Jeśli dobrze znasz JavaScript, robiłeś trochę TypeScriptu i chcesz utrwalić swój model mentalny, to jest dla Ciebie. Pełna historia jest w Podręczniku TypeScript — ten artykuł to wersja 80/20.

Typy prymitywne i literały

Typy prymitywne TypeScriptu mapują się bezpośrednio na typy środowiska uruchomieniowego JavaScript: string, number, boolean, null, undefined, bigint i symbol. Pierwsze trzy będziesz używać stale; pozostałe pojawiają się w konkretnych kontekstach.

ts
// Primitives — the foundation
let userId: number = 42;
let username: string = 'alice';
let isActive: boolean = true;
let deletedAt: Date | null = null;     // nullable pattern
let refreshToken: string | undefined;  // optional pattern

Gdzie TypeScript staje się interesujący, to typy literałowe. Zamiast po prostu mówić, że wartość jest string, możesz powiedzieć, że musi być jednym z konkretnego zestawu ciągów. Jest to znacznie bardziej użyteczne niż prosta adnotacja string, ponieważ TypeScript może wyłapywać nieprawidłowe wartości w czasie kompilacji:

ts
type Direction = 'north' | 'south' | 'east' | 'west';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500;

function navigate(dir: Direction) {
  console.log(`Moving ${dir}`);
}

navigate('north');  // ✅
navigate('up');     // ❌ Argument of type '"up"' is not assignable to type 'Direction'

// Combine with string for "known values plus free-form"
type EventName = 'click' | 'focus' | 'blur' | (string & {});
Sztuczka (string & {}) na końcu sprawia, że autouzupełnianie działa dla znanych wartości, jednocześnie akceptując dowolny ciąg. To powszechny wzorzec w bibliotekach systemów projektowania.

interface kontra type — kiedy używać którego

To jest pytanie, które zadaje każdy początkujący TypeScript. Praktyczna odpowiedź: używaj interface dla kształtów obiektów — szczególnie tych reprezentujących publiczne kontrakty API lub takich, które inne typy będą rozszerzać. Używaj type dla unii, przecięć, typów mapowanych i wszystkiego, co nie jest czysto kształtem obiektu. W praktyce są w dużej mierze zamienne dla kształtów obiektów — wybierz jedną konwencję i bądź konsekwentny w bazie kodu.

ts
// interface — object shapes, extensible via extends
interface User {
  id: number;
  email: string;
  createdAt: Date;
}

interface AdminUser extends User {
  role: 'admin';
  permissions: string[];
}

// type — unions, intersections, aliases for anything
type ID = string | number;
type Nullable<T> = T | null;

// Intersection — combine two shapes (common for mixins / HOC props)
type WithTimestamps = {
  createdAt: Date;
  updatedAt: Date;
};

type UserRecord = User & WithTimestamps;

// type is required here — interface can't express a union of shapes
type ApiResponse<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; error: string; code: number };

Jedna istotna różnica: interfejsy obsługują scalanie deklaracji — możesz dwukrotnie zadeklarować ten sam interfejs, a TypeScript scali definicje. W ten sposób biblioteki rozszerzają typy globalne (np. dodając właściwości do Window). Typy się nie scalają; ponowna deklaracja typu jest błędem.

Typy unii i przecięcia

Typy unii (A | B) mówią „ta wartość jest albo A, albo B". Typy przecięcia (A & B) mówią „ta wartość jest jednocześnie zarówno A, jak i B". Unie są wszędzie w prawdziwym kodzie — najpotężniejszym wzorcem, który umożliwiają, jest unia dyskryminowana, która pozwala modelować dane mogące być w różnych stanach bez uciekania się do opcjonalnych pól wszędzie.

ts
// Discriminated union — model API response states cleanly
type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string; retryable: boolean };

// TypeScript narrows the type inside each branch
function render<T>(state: FetchState<T>) {
  switch (state.status) {
    case 'idle':    return '<p>Not started</p>';
    case 'loading': return '<p>Loading...</p>';
    case 'success': return `<pre>${JSON.stringify(state.data)}</pre>`;
    case 'error':   return `<p>Error: ${state.error}</p>`;
  }
}

// Intersection — common in React/Angular for composing prop types
type ButtonBaseProps = {
  label: string;
  disabled?: boolean;
};

type IconButtonProps = ButtonBaseProps & {
  icon: string;
  iconPosition: 'left' | 'right';
};

Wzorzec unii dyskryminowanej opiera się na dyskryminancie — właściwości literałowej (tutaj status), która jest unikalna dla każdego wariantu. TypeScript używa tej właściwości do zawężenia pełnego typu wewnątrz gałęzi warunkowych, dając bezpieczny dostęp do pól specyficznych dla wariantu, takich jak data lub error.

Generyki — część, która wszystkich trips up

Motywujący problem: chcesz funkcję, która działa na wielu typach, ale użycie any usuwa wszystkie informacje o typach. Generyki to rozwiązują — to parametry typów, pisane w nawiasach kątowych, które pozwalają funkcji lub interfejsowi działać na rodzinie typów przy zachowaniu pełnego bezpieczeństwa typów.

ts
// Without generics — any loses all information
function first(arr: any[]): any {
  return arr[0];
}
const x = first([1, 2, 3]); // x is 'any' — TypeScript can't help you here

// With generics — T is inferred from the argument
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
const n = first([1, 2, 3]);       // n: number | undefined ✅
const s = first(['a', 'b', 'c']); // s: string | undefined ✅

Składnia <T> deklaruje parametr typu. Możesz nadać mu dowolną nazwę — T to tylko konwencja dla pojedynczego typu generycznego. Gdy wywołujesz funkcję, TypeScript wnioskuje T z argumentów, więc rzadko musisz pisać go wprost. Generyczne interfejsy są równie użyteczne do modelowania kształtów API:

ts
// Generic interface — the API wrapper pattern every codebase has
interface ApiResponse<T> {
  data: T;
  meta: {
    page: number;
    totalPages: number;
    totalItems: number;
  };
}

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

// The response type is fully typed — no casting needed
async function fetchUsers(): Promise<ApiResponse<User[]>> {
  const res = await fetch('/api/users');
  return res.json();
}

const response = await fetchUsers();
response.data[0].email; // ✅ TypeScript knows this is a string
response.meta.totalPages; // ✅ TypeScript knows this is a number

// Constrained generics — T must have an 'id' field
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

const user = findById(users, 42); // T inferred as User
Kiedy sięgnąć po generyki: gdy zauważasz, że piszesz tę samą sygnaturę funkcji dwa razy z różnymi typami, lub rzutujesz za pomocą as, aby uzyskać żądany typ zwracany. To są dwa sygnały, że generyk byłby czystszy.

Typy narzędziowe, których naprawdę będziesz używać

TypeScript dostarcza zestaw wbudowanych typów narzędziowych, które przekształcają istniejące typy w nowe. Eliminują potrzebę ręcznego duplikowania lub modyfikowania definicji typów. Oto te, które stale pojawiają się w prawdziwych bazach kodu:

ts
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
  createdAt: Date;
}

// Partial<T> — all properties become optional
// Perfect for PATCH/update payloads
type UpdateUserPayload = Partial<User>;
// { id?: number; name?: string; email?: string; ... }

async function updateUser(id: number, payload: Partial<User>) {
  return fetch(`/api/users/${id}`, {
    method: 'PATCH',
    body: JSON.stringify(payload)
  });
}

// Required<T> — all properties become required (opposite of Partial)
interface Config {
  apiUrl?: string;
  timeout?: number;
  retries?: number;
}
type ResolvedConfig = Required<Config>;
// { apiUrl: string; timeout: number; retries: number }

// Pick<T, K> — keep only specified properties
type UserSummary = Pick<User, 'id' | 'name' | 'email'>;
// Use in list views where you only need the display fields

// Omit<T, K> — exclude specified properties
type PublicUser = Omit<User, 'role' | 'createdAt'>;
// Safe to expose in API responses

// Readonly<T> — prevents mutation (great for config and frozen state)
const config: Readonly<ResolvedConfig> = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};
// config.timeout = 10000; // ❌ Cannot assign to 'timeout' because it is a read-only property

// Record<K, V> — typed dictionary
type UserCache = Record<number, User>;
const cache: UserCache = {};
cache[42] = { id: 42, name: 'Alice', email: '[email protected]', role: 'user', createdAt: new Date() };

// Common pattern: mapping string keys to a known value shape
type FeatureFlags = Record<string, { enabled: boolean; rolloutPct: number }>;

// ReturnType<T> — infer the return type of a function
function createSession(userId: number) {
  return {
    token: crypto.randomUUID(),
    userId,
    expiresAt: new Date(Date.now() + 86_400_000)
  };
}

type Session = ReturnType<typeof createSession>;
// { token: string; userId: number; expiresAt: Date }
// — no need to define the type separately and keep it in sync

unknown kontra any kontra never

Te trzy typy mylą większość programistów, dopóki nie zrozumieją, jaki problem każdy z nich rozwiązuje. any to furtka — całkowicie wyłącza sprawdzanie typów dla tej wartości. Jest użyteczne przy migracji z JavaScript do TypeScript, ale nadużywanie pokonuje cel TypeScriptu. unknown to bezpieczna alternatywa dla typów: wartość może być czymkolwiek, ale musisz ją zawęzić zanim cokolwiek z nią zrobisz. never to typ, który nigdy nie może wystąpić — dno hierarchii typów.

ts
// any — type checking disabled, use sparingly
function dangerousTransform(input: any) {
  return input.toUpperCase(); // TypeScript won't warn even if this crashes at runtime
}

// unknown — safe alternative, forces you to check before using
function safeTransform(input: unknown): string {
  if (typeof input === 'string') {
    return input.toUpperCase(); // ✅ narrowed to string inside this block
  }
  if (typeof input === 'number') {
    return input.toFixed(2);    // ✅ narrowed to number
  }
  throw new Error(`Cannot transform value of type ${typeof input}`);
}

// Common use: parsing untrusted data (API responses, localStorage, user input)
function parseConfig(raw: unknown): ResolvedConfig {
  if (
    typeof raw === 'object' &&
    raw !== null &&
    'apiUrl' in raw &&
    typeof (raw as any).apiUrl === 'string'
  ) {
    return raw as ResolvedConfig;
  }
  throw new Error('Invalid config format');
}

// never — the exhaustive switch pattern
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.radius ** 2;
    case 'square': return shape.side ** 2;
    default:
      // If you add a new Shape variant and forget to handle it here,
      // TypeScript will error: Type 'NewShape' is not assignable to type 'never'
      const _exhaustive: never = shape;
      throw new Error(`Unhandled shape: ${_exhaustive}`);
  }
}

Zawężanie typów w praktyce

Zawężanie typów to sposób, w jaki TypeScript doprecyzowuje szeroki typ (string | number, unknown, unia dyskryminowana) do konkretnego typu wewnątrz bloku warunkowego. Dokumentacja zawężania TypeScript omawia każdą strażnicę szczegółowo — oto wzorce, które będziesz pisać codziennie.

ts
// typeof — primitive narrowing
function formatValue(val: string | number | boolean): string {
  if (typeof val === 'string')  return val.trim();
  if (typeof val === 'number')  return val.toLocaleString();
  return val ? 'Yes' : 'No';
  // TypeScript knows val must be boolean here
}

// instanceof — class/object narrowing
function handleError(err: unknown): string {
  if (err instanceof Error)   return err.message;
  if (typeof err === 'string') return err;
  return 'An unknown error occurred';
}

// in operator — property existence check
type Cat = { meow(): void };
type Dog = { bark(): void };

function makeNoise(animal: Cat | Dog) {
  if ('meow' in animal) {
    animal.meow(); // TypeScript narrows to Cat
  } else {
    animal.bark(); // TypeScript narrows to Dog
  }
}

// Discriminated union narrowing (most common in component state)
type LoadState<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; message: string };

function renderState<T>(state: LoadState<T>) {
  if (state.status === 'success') {
    console.log(state.data);    // ✅ data is accessible
  } else if (state.status === 'error') {
    console.error(state.message); // ✅ message is accessible
  }
}

// Type predicates — custom type guards
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'email' in obj
  );
}

// After isUser() returns true, TypeScript knows obj is User
function processPayload(payload: unknown) {
  if (isUser(payload)) {
    console.log(payload.email); // ✅ fully typed
  }
}

Operator typeof to najbardziej podstawowe narzędzie zawężania. W połączeniu z instanceof, operatorem in i kontrolami właściwości dyskryminujących możesz obsłużyć praktycznie każdy scenariusz zawężania bez sięgania po any lub niebezpieczne rzutowania.

Podsumowanie

System typów TypeScriptu nagradza dobre opanowanie podstaw bardziej niż zapamiętywanie niejasnych funkcji. Typy literałowe i unie dyskryminowane eliminują całe kategorie błędów wykonawczych. Generyki pozwalają pisać wielokrotnego użytku, bezpieczne abstrakcje. Typy narzędziowe takie jak Partial, Pick, Omit i ReturnType sprawiają, że Twoje typy są DRY. A unknown z zawężaniem daje bezpieczeństwo typów nawet na krawędziach, gdzie dane przychodzą z zewnątrz. Najlepszym sposobem na utrwalenie tego wszystkiego jest interaktywne eksperymentowanie — Plac zabaw TypeScript pozwala wkleić dowolny fragment i zobaczyć wnioskowane typy natychmiast, bez konfiguracji. Gdy pracujesz z danymi JSON w projektach TypeScript, narzędzia Formater JSON i JSON do TypeScript na tej stronie mogą automatycznie generować definicje interfejsów z rzeczywistych ładunków API.