Czym jest TypeScript i dlaczego warto go używać
TypeScript to nadzbiór języka JavaScript, który dodaje statyczne typowanie oraz nowoczesne mechanizmy znane z języków takich jak C# czy Java. Projekt stworzony przez Microsoft w 2012 roku stał się dziś de facto standardem w większych projektach frontendowych i backendowych. Kod TypeScript kompiluje się do czystego JavaScriptu, więc uruchomisz go wszędzie tam, gdzie działa zwykły JS — w przeglądarce, Node.js, Denie czy Bunie.
Największym atutem TS-a jest wychwytywanie błędów na etapie pisania kodu, zanim w ogóle uruchomisz aplikację. Zamiast tropić w produkcji undefined is not a function, dostajesz czerwone podkreślenie w edytorze i konkretny komunikat. Drugi argument to wsparcie IDE: autouzupełnianie, inteligentny refactoring, nawigacja po kodzie i dokumentacja widoczna w podpowiedziach. Jeśli kiedykolwiek zmieniałeś nazwę pola w dużym projekcie JS-owym i modliłeś się, żeby niczego nie przeoczyć — TypeScript rozwiąże ten problem.
W tym artykule poznasz konfigurację kompilatora, typy podstawowe i zaawansowane, generyki, narzędzia pomocnicze (utility types), dyskryminowane unie oraz strategie migracji istniejącego kodu JavaScript. Artykuł zakłada, że znasz już podstawy JavaScriptu — jeśli nie, zacznij od tamtego materiału.
Instalacja i konfiguracja
Kompilator tsc i tsconfig.json
Zaczynasz od instalacji kompilatora. Wystarczy globalne npm install -g typescript albo — co zdecydowanie zalecam — dodanie go jako zależność rozwojową projektu. Kompilator nazywa się tsc i odpowiada za zamianę plików .ts na .js. Konfiguracja projektu żyje w pliku tsconfig.json, który tworzysz poleceniem tsc --init.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Najważniejsze opcje to target (wersja JS generowana na wyjściu), module (system modułów — ESNext, CommonJS, NodeNext), strict (włącza wszystkie restrykcyjne sprawdzenia) oraz outDir i rootDir. Włączenie strict: true aktywuje pakiet flag: strictNullChecks, noImplicitAny, strictFunctionTypes i kilka innych. Traktuj to jako obowiązkowe ustawienie w nowym projekcie.
W nowoczesnym setupie z Vite, Next.js czy Astro to bundler transpiluje TypeScript, a tsc uruchamiasz tylko z flagą --noEmit do sprawdzania typów. Rozdzielenie transpilacji od type-checkingu znacząco przyspiesza build.
Typy podstawowe i zaawansowane
Prymitywy, tablice, krotki
TypeScript zna wszystkie prymitywy JS-a: string, number, boolean, null, undefined, bigint, symbol. Dochodzą specjalne typy any (wyłącza sprawdzanie — używaj ostrożnie), unknown (bezpieczniejsza alternatywa dla any), never (nigdy nie występuje) i void (funkcja nic nie zwraca).
// Prymitywy z jawną adnotacją typu
const imie: string = "Marek";
const wiek: number = 32;
const aktywny: boolean = true;
// Tablice - dwa równoważne zapisy
const liczby: number[] = [1, 2, 3];
const nazwy: Array<string> = ["Ola", "Tomek"];
// Krotka - tablica o ustalonej długości i typach
const punkt: [number, number] = [10, 20];
const rekord: [string, number, boolean] = ["Ania", 28, true];
// Typy literałowe - tylko konkretne wartości
type Kierunek = "polnoc" | "poludnie" | "wschod" | "zachod";
const ruch: Kierunek = "polnoc"; // OK
// const zly: Kierunek = "gora"; // Error!
Typy literałowe to jedna z najprzyjemniejszych cech TypeScriptu — pozwalają ograniczyć wartości do konkretnego zbioru, bez potrzeby deklarowania enuma. Połączone z unią dają potężne narzędzie do modelowania stanów aplikacji.
Zawężanie typów i type guards
Kiedy zmienna może mieć kilka typów (unia), TypeScript potrafi zawęzić typ na podstawie sprawdzeń w kodzie. Nazywamy to narrowing. Klasyczne narzędzia to typeof, instanceof, sprawdzanie in oraz własne type guards zwracające x is Typ.
function formatuj(wartosc: string | number): string {
if (typeof wartosc === "string") {
// Tutaj wartosc ma typ string
return wartosc.toUpperCase();
}
// Tutaj wartosc ma typ number
return wartosc.toFixed(2);
}
// Własny type guard
interface Pies { szczekaj: () => void; }
interface Kot { miaucz: () => void; }
function jestPsem(zwierze: Pies | Kot): zwierze is Pies {
return "szczekaj" in zwierze;
}
Interfejsy, typy i generyki
interface vs type
Do opisu kształtu obiektu możesz użyć interface lub type. Różnice są subtelne: interfejs można rozszerzać przez extends i deklarować wielokrotnie (merging), a aliasy typów obsługują unie, intersekcje i typy mapowane. W praktyce dla obiektów i klas wybieraj interface, a dla unii, krotek i bardziej wyrafinowanych konstrukcji — type.
// Interface z opcjonalnym polem i readonly
interface Uzytkownik {
readonly id: number;
email: string;
imie?: string;
rola: "admin" | "user" | "guest";
}
// Rozszerzanie interfejsu
interface Administrator extends Uzytkownik {
uprawnienia: string[];
}
// Generyczna funkcja - T to parametr typu
function pierwszy<T>(tablica: T[]): T | undefined {
return tablica[0];
}
const liczba = pierwszy([1, 2, 3]); // number | undefined
const tekst = pierwszy(["a", "b", "c"]); // string | undefined
// Generyki z ograniczeniem (constraint)
function dlugosc<T extends { length: number }>(x: T): number {
return x.length;
}
dlugosc("hello"); // OK
dlugosc([1, 2, 3]); // OK
// dlugosc(42); // Error - number nie ma length
// Generyczna klasa
class Pudelko<T> {
constructor(private zawartosc: T) {}
pobierz(): T { return this.zawartosc; }
}
Generyki to jeden z najpotężniejszych elementów TypeScriptu. Pozwalają pisać kod wielokrotnego użytku, który zachowuje informacje o typie. Zamiast tracić typ używając any, parametryzujesz funkcję lub klasę literą T (z ang. type), a kompilator sam wywnioskuje konkretny typ przy wywołaniu.
Utility types i dyskryminowane unie
Narzędzia pomocnicze
TypeScript dostarcza zestaw wbudowanych utility types, które transformują istniejące typy. Oszczędzają masę pisania i pomagają zachować pojedyncze źródło prawdy o kształcie danych.
interface Produkt {
id: number;
nazwa: string;
cena: number;
opis: string;
dostepny: boolean;
}
// Partial - wszystkie pola opcjonalne (np. do update)
type AktualizacjaProduktu = Partial<Produkt>;
// Pick - wybieramy tylko kilka pol
type PodgladProduktu = Pick<Produkt, "id" | "nazwa" | "cena">;
// Omit - usuwamy konkretne pola
type NowyProdukt = Omit<Produkt, "id">;
// Record - mapa klucz->wartosc
type Slownik = Record<string, number>;
type StatusyKolor = Record<"ok" | "blad" | "info", string>;
// ReturnType i Parameters - wyciagamy sygnature funkcji
function pobierzUzytkownika(id: number) {
return { id, nazwa: "Ania", email: "ania@example.com" };
}
type UzytkownikDTO = ReturnType<typeof pobierzUzytkownika>;
type Argumenty = Parameters<typeof pobierzUzytkownika>;
Dyskryminowane unie
Dyskryminowana unia to kilka typów połączonych przez wspólne pole rozróżniające (najczęściej kind lub type). TypeScript dzięki niemu potrafi precyzyjnie zawęzić typ wewnątrz bloku switch, eliminując niemożliwe kombinacje pól.
type StanZadania =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; dane: string[] }
| { status: "error"; komunikat: string };
function renderuj(stan: StanZadania): string {
switch (stan.status) {
case "idle":
return "Gotowe do akcji";
case "loading":
return "Ładowanie...";
case "success":
// Tutaj TS wie, ze jest pole 'dane'
return `Pobrano ${stan.dane.length} elementów`;
case "error":
return `Błąd: ${stan.komunikat}`;
}
}
// Bezpieczna obsluga null/undefined
const uzytkownik: { imie?: string } | null = pobierz();
const dlugosc = uzytkownik?.imie?.length ?? 0; // optional chaining + nullish coalescing
Operatory ?. (optional chaining) i ?? (nullish coalescing) to idealne uzupełnienie strictNullChecks. Zamiast piętrowych if (x && x.y && x.y.z) piszesz x?.y?.z, a zamiast x || default używasz x ?? default, żeby 0 i "" nie wywracały logiki.
Integracja z frameworkami
TypeScript świetnie współpracuje ze wszystkimi popularnymi frameworkami. React ma pełne wsparcie dzięki @types/react: komponenty typujesz przez React.FC lub bezpośrednio propsy, hooki automatycznie wnioskują typy. Vue 3 z Composition API i <script setup lang="ts"> daje spójne typowanie refów i propsów. W Node.js instalujesz @types/node i masz dostęp do typów dla fs, http, process.
import { ReactNode } from "react";
interface ButtonProps {
children: ReactNode;
variant?: "primary" | "secondary";
disabled?: boolean;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
}
export function Button({
children,
variant = "primary",
disabled = false,
onClick,
}: ButtonProps) {
return (
<button
className={`btn btn--${variant}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
Migracja z JavaScriptu i pułapki
Strategia stopniowej migracji
Nie musisz przepisywać całego projektu w jeden weekend. TypeScript wspiera stopniową migrację dzięki opcji allowJs: true w tsconfig.json — projekt może zawierać mieszane pliki .js i .ts. Dobry plan wygląda tak: włącz TypeScript bez strict, zmieniaj rozszerzenie pojedynczych plików na .ts, dodawaj typy, a na końcu włączaj strict: true i poprawiaj to, co jeszcze nie pasuje. Opcja checkJs pozwala typować pliki JS przy pomocy komentarzy JSDoc bez zmiany rozszerzenia.
Typowe pułapki
Największym grzechem początkujących jest nadużywanie any. Za każdym razem, gdy wpisujesz any, tracisz wszystkie korzyści TypeScriptu w tym fragmencie. Jeśli nie znasz typu, użyj unknown — zmusi cię do sprawdzenia typu przed użyciem wartości. Drugi problem to asercje typów (wartosc as Typ) stosowane zamiast type guards. Asercja to obietnica, że wiesz lepiej niż kompilator — jeśli się mylisz, błąd wybuchnie w runtime. Preferuj walidację przez bibliotekę w rodzaju zod lub własny type guard.
Nie używaj as any w połączeniu z rzutowaniami w stylu wartosc as unknown as Jakis — to sygnał, że typy w twoim kodzie kłamią. Zatrzymaj się, zrozum, dlaczego kompilator protestuje, i napraw źródłowy typ, zamiast maskować problem.
Narzędzia w ekosystemie
Surowy tsc to dopiero początek. Standardowy zestaw narzędziowy każdego projektu TS-owego obejmuje:
- ESLint z pluginem
@typescript-eslint— wykrywa błędy stylu i potencjalne bugi wykraczające poza zakres kompilatora. - Prettier — automatyczne formatowanie kodu, eliminuje spory o tabulatory i średniki na code review.
- ts-node lub tsx — uruchamiają pliki
.tsbezpośrednio w Node.js bez oddzielnego kroku kompilacji, idealne do skryptów. - vite-node — szybszy runner TS-a wykorzystujący pipeline Vite, świetny do testów i szybkich iteracji.
- Vitest / Jest z
ts-jest— testy jednostkowe z pełnym wsparciem typów. - tsup / unbuild / Rollup — buildery bibliotek, generują pliki
.d.tsdla użytkowników końcowych.
Dla IDE wybierz Visual Studio Code — silnik TypeScriptu jest w nim natywny, a podpowiedzi i refactoring działają błyskawicznie. JetBrains WebStorm również oferuje świetne wsparcie, choć dla TS-a VS Code zwykle wygrywa szybkością.
Teraz gdy znasz podstawy, zbuduj mały projekt — API Express, komponent React, skrypt CLI — w pełni w TypeScripcie od pierwszej linijki z strict: true. Nic tak nie uczy jak praktyka. Więcej o naszym podejściu do edukacji programistów znajdziesz na stronie o nas.

