Przejdź do treści
← wszystkie przedmioty

TypeScript

Głęboka semantyka systemu typów TypeScript 5.x: typowanie strukturalne, zawężanie, unie dyskryminowane, generyki, typy warunkowe, mapowane i szablonowe.

TEMATY EGZAMINACYJNE W TYM PRZEDMIOCIE
  • Structural typing vs nominal typing i konsekwencje dla API
  • unknown vs any vs never, kiedy który jest poprawny
  • Discriminated unions + exhaustiveness checking przez never
  • Generics z constraints, conditional types i infer
  • satisfies operator vs adnotacja typu vs as
  • Mapped types z key remapping (as clause) i utility types

1 · Typowanie strukturalne i podstawy systemu typów

★ egzamin

TypeScript nie sprawdza typów po nazwie deklaracji, tylko po kształcie danych. To fundament odróżniający go od Javy czy C#, i źródło większości zaskoczeń u programistów przychodzących z języków nominalnie typowanych.

Termin kluczowy
Structural typing (duck typing) - Dwa typy są kompatybilne, jeśli mają ten sam kształt (te same właściwości o kompatybilnych typach), niezależnie od nazwy interfejsu czy klasy, którą deklarujesz.
Kompatybilność strukturalna niezależna od nazwy typu typescript
interface Point2D {
  x: number;
  y: number;
}

interface Vector {
  x: number;
  y: number;
}

function length(p: Point2D): number {
  return Math.sqrt(p.x ** 2 + p.y ** 2);
}

const v: Vector = { x: 3, y: 4 };
length(v); // OK, Vector ma dokładnie kształt Point2D, nazwa nie ma znaczenia

// Excess property check: literał obiektowy jest sprawdzany ściślej niż zmienna
length({ x: 3, y: 4, z: 0 }); // Error: 'z' nie istnieje w Point2D
const withZ = { x: 3, y: 4, z: 0 };
length(withZ); // OK, bo withZ przechodzi przez zmienną (nie fresh object literal)
Excess property checking
Kompilator dodatkowo sprawdza 'świeże' literały obiektowe pod kątem nadmiarowych właściwości, ale to osobny, ostrzejszy mechanizm nałożony na strukturalną kompatybilność, nie jej część. Przypisanie przez zmienną pośrednią omija tę kontrolę.
Pułapka
Programiści z tłem Java/C# zakładają, że interfejs działa jak kontrakt nominalny (trzeba explicit 'implements'). W TS wystarczy pasujący kształt, co ułatwia integrację z bibliotekami trzecimi, ale też pozwala przypadkowo przepuścić obiekt niewłaściwego 'logicznie' typu, jeśli kształt się zgadza.
Termin kluczowy
Type inference - Kompilator wywnioskowuje typ zmiennej z jej inicjalizacji bez jawnej adnotacji. Preferuj inference nad jawnymi adnotacjami tam, gdzie nie tracisz precyzji, kod jest krótszy i typ nadal poprawny.
Kontrawariancja parametrów funkcji przy strukturalnej zgodności typescript
type Handler = (e: { type: string }) => void;

const onClick: Handler = (e: { type: string; x: number; y: number }) => {
  console.log(e.x, e.y);
};
// Błąd w strict mode: parametr Handler nie gwarantuje pól x/y
  • Struktura decyduje o kompatybilności, nie nazwa typu ani hierarchia dziedziczenia.
  • Excess property checking działa tylko na świeżych literałach obiektowych, nie na zmiennych.
  • Type inference redukuje szum, ale w publicznym API funkcji warto adnotować jawnie dla stabilności kontraktu.
  • Strukturalna zgodność funkcji wymaga zgodności parametrów i typu zwracanego, nie tylko nazwy sygnatury.
Mnemonik
TypeScript pyta 'czy wygląda jak kaczka i kwacze jak kaczka', nie 'czy nazywa się Kaczka'.

2 · any, unknown i never: trzy skrajne typy

★ egzamin

any, unknown i never leżą na przeciwległych krańcach systemu typów: any wyłącza kontrolę typów, unknown wymusza jej odzyskanie przed użyciem, never reprezentuje wartość, która nigdy nie powstanie.

Termin kluczowy
any - Ucieczkowy typ wyłączający type checking dla danej wartości. Przypisywalny do/z dowolnego typu bez żadnej walidacji. Używać wyłącznie jako świadomy wyjątek (np. brzeg z nietypowaną biblioteką JS), nigdy jako domyślne rozwiązanie problemu z typami.
Termin kluczowy
unknown - Type-safe odpowiednik any: przyjmuje dowolną wartość, ale nie pozwala na żadną operację (właściwości, wywołanie, indeksowanie) bez wcześniejszego zawężenia typu (type guard, instanceof, typeof).
Termin kluczowy
never - Typ pustego zbioru wartości, brak żadnej możliwej wartości. Zwracany przez funkcje, które zawsze rzucają wyjątek lub zapętlają się w nieskończoność; reprezentuje też 'niemożliwą' gałąź w exhaustiveness checking.
unknown wymusza zawężenie przed użyciem typescript
function parseConfig(raw: unknown): string {
  if (typeof raw !== 'string') {
    throw new TypeError('config must be a string');
  }
  return raw.toUpperCase(); // OK, raw zawężony do string
}

function unsafeParse(raw: any): string {
  return raw.toUpperCase(); // kompiluje się, ale crashuje w runtime dla raw = 42
}
never jako sygnał nieosiągalnego kodu typescript
function assertUnreachable(x: never): never {
  throw new Error(`Unexpected value: ${JSON.stringify(x)}`);
}

function fail(message: string): never {
  throw new Error(message);
}

// zmienna typu never akceptuje tylko wartości typu never
function process(input: string | number) {
  if (typeof input === 'string') return input.length;
  if (typeof input === 'number') return input * 2;
  return assertUnreachable(input); // input tutaj ma typ never, bo obie gałęzie wyczerpane
}
TypPrzyjmuje dowolną wartość?Operacje bez zawężeniaTypowy use case
anyTakWszystkie, bez kontroliMigracja z JS, biblioteki bez typów (unikać docelowo)
unknownTakBrak, wymaga narrowingDane z zewnątrz: JSON.parse, odpowiedzi API, argumenty catch
neverNie (zbiór pusty)Brak, nic nie da się przypisać do niczego innego niż neverExhaustiveness checking, funkcje zawsze rzucające
Pułapka
catch (e) w TS 4.4+ domyślnie typuje 'e' jako unknown (przy useUnknownInCatchVariables), nie any. Kod zakładający 'e.message' bez zawężenia (np. e instanceof Error) nie skompiluje się w strict setupie i słusznie, bo w JS można rzucić cokolwiek, nie tylko Error.
Zero-cost abstraction
any i unknown nie generują żadnego kodu runtime, to czysto kompilacyjne konstrukty. never analogicznie znika po kompilacji do JS, ale wpływa na to, czy compiler w ogóle pozwoli kod zbudować.

3 · Zawężanie typów i user-defined type guards

★ egzamin

Narrowing to proces, w którym compiler zawęża szeroki typ (unię) do węższego wewnątrz konkretnej gałęzi kodu, na podstawie kontrolnego przepływu: typeof, instanceof, in, porównania równości, czy własnych predykatów.

Termin kluczowy
Type guard - Wyrażenie, które compiler rozpoznaje jako dowód zawężenia typu w danym bloku kodu. Wbudowane: typeof, instanceof, in, Array.isArray, porównania z null/undefined.
Termin kluczowy
User-defined type guard (predicate function) - Funkcja zwracająca 'parameterName is Type' zamiast boolean, informująca compiler, że pozytywny wynik funkcji zawęża typ argumentu do wskazanego typu we wszystkich miejscach wywołania.
Wbudowane narrowing: typeof, in, instanceof typescript
type Input = string | number | Date;

function format(value: Input): string {
  if (typeof value === 'string') return value.trim();
  if (typeof value === 'number') return value.toFixed(2);
  return value.toISOString(); // zostaje tylko Date
}

interface Dog { bark(): void }
interface Cat { meow(): void }

function speak(animal: Dog | Cat) {
  if ('bark' in animal) {
    animal.bark(); // zawężone do Dog
  } else {
    animal.meow(); // zawężone do Cat
  }
}
User-defined type guard z predykatem 'is' typescript
interface ApiError {
  code: number;
  message: string;
}

function isApiError(value: unknown): value is ApiError {
  return (
    typeof value === 'object' &&
    value !== null &&
    'code' in value &&
    'message' in value
  );
}

async function handle(response: unknown) {
  if (isApiError(response)) {
    console.error(response.code, response.message); // zawężone do ApiError
    return;
  }
  console.log('ok', response);
}
asserts is (5.0+)
Poza 'value is Type' istnieje wariant asercyjny 'function assertIsString(x: unknown): asserts x is string'. Zamiast zwracać boolean, funkcja rzuca wyjątek gdy warunek nie zachodzi, a compiler zawęża typ po wywołaniu, nie w gałęzi if.
Assertion function zawężająca po wywołaniu, nie w if typescript
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
  if (value === null || value === undefined) {
    throw new Error('Value must be defined');
  }
}

function load(id: string | undefined) {
  assertIsDefined(id);
  return id.toUpperCase(); // id zawężone do string po tej linii
}
Pułapka
Type guard z błędną implementacją (np. sprawdza tylko 'code' in value, a nie sprawdza typu message) kłamie compilerowi, TS ufa deklarowanemu predykatowi bez weryfikacji jego ciała. Błąd w guard nie da żadnego błędu kompilacji, tylko crash w runtime później.
  1. 1 Zdefiniuj predykat 'x is Type' zamiast zwracać boolean, gdy wynik ma zawężać typ dla wywołującego.
  2. 2 Sprawdź każde pole używane dalej, guard musi realnie gwarantować kształt, nie tylko część.
  3. 3 Preferuj discriminated union nad ręcznym type guard, gdy to możliwe, compiler zawęzi automatycznie po polu-discriminancie.

4 · Discriminated unions i exhaustiveness checking

★ egzamin

Discriminated union (tagged union) to wzorzec, w którym każdy człon unii ma wspólne pole-literal (discriminant), pozwalające compilerowi automatycznie zawężać typ w switch/if bez ręcznych type guardów.

Termin kluczowy
Discriminant property - Pole obecne w każdym wariancie unii, o typie unikalnego literału (string, number lub boolean literal), używane jako 'tag' identyfikujący konkretny wariant.
Discriminated union z polem 'kind' typescript
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default: {
      const exhaustiveCheck: never = shape;
      throw new Error(`Unhandled shape: ${exhaustiveCheck}`);
    }
  }
}
Exhaustiveness checking
Gałąź 'default' przypisująca 'shape' do zmiennej typu never kompiluje się tylko wtedy, gdy wszystkie warianty unii zostały obsłużone wcześniej. Dodanie nowego wariantu do Shape bez obsługi w switch spowoduje błąd kompilacji w tej linii, to najsilniejsza forma statycznej gwarancji kompletności obsługi.
Efekt dodania nowego wariantu: błąd kompilacji w miejscu exhaustiveness check typescript
type Shape2 = Shape | { kind: 'square'; side: number };

function area2(shape: Shape2): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.radius ** 2;
    case 'rectangle': return shape.width * shape.height;
    case 'triangle': return (shape.base * shape.height) / 2;
    default: {
      // Error: Type '{ kind: "square"; side: number }' is not assignable to type 'never'
      const exhaustiveCheck: never = shape;
      throw new Error(String(exhaustiveCheck));
    }
  }
}
Pułapka
Bez pola-discriminanta (np. dwa warianty rozróżnione tylko obecnością różnych pól bez wspólnego tagu) narrowing bywa niepełny lub wymaga ręcznych 'in' checks, znacznie łatwiej o pominiętą gałąź, którą compiler nie wykryje jako błąd.
  • Zawsze dodawaj literal discriminant (np. 'type' albo 'kind') do wariantów unii reprezentujących stan/zdarzenie.
  • Kończ switch/if-chain gałęzią z assignmentem do never, to zamienia brak obsługi w błąd kompilacji, nie w bug produkcyjny.
  • Redux, state machines i event handling to naturalne miejsca dla discriminated unions.
Mnemonik
Discriminant + never w default = kompilator jako automatyczny reviewer kompletności switcha.

5 · Generics: constraints, wnioskowanie, const type parameters

★ egzamin

Generyki parametryzują typy i funkcje bez utraty precyzji typowej, w przeciwieństwie do any, który tę precyzję zeruje. Constraints (extends) ograniczają, jakie typy wolno podstawić, keyof i indexed access pozwalają operować na kształcie obiektu w sposób type-safe.

Termin kluczowy
Generic constraint (extends) - Ogranicza parametr typu do podzbioru typów spełniających daną strukturę, np. <T extends { id: string }> pozwala bezpiecznie odwołać się do 'id' wewnątrz funkcji.
Constraint z keyof dla bezpiecznego dostępu do właściwości typescript
function getProperty<T extends object, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 'u1', name: 'Ada', age: 30 };
const name = getProperty(user, 'name'); // typ: string
// getProperty(user, 'email'); // Error: 'email' nie jest kluczem user
Termin kluczowy
const type parameter (TS 5.0+) - Modyfikator 'const' na parametrze typu (<const T>) wymusza wnioskowanie literalnego (najwęższego) typu argumentu zamiast jego rozszerzonej wersji, bez konieczności ręcznego 'as const' w miejscu wywołania.
const type parameter: literal inference bez 'as const' u wywołującego typescript
function tuple<const T extends readonly unknown[]>(...items: T): T {
  return items;
}

const a = tuple('a', 'b', 'c');
// bez 'const': typ [string, string, string] (tuple z widened typami, nie string[])
// z 'const': typ readonly ["a", "b", "c"]

declare function makeConfig<const T>(config: T): T;
const config = makeConfig({ mode: 'production', retries: 3 });
// config.mode: "production" (literal), nie string
Domyślne wartości parametrów typu
Podobnie jak parametry funkcji, parametry typu mogą mieć wartość domyślną: <T = string>, używaną gdy caller nie poda ani argumentu typu, ani nie da się go wywnioskować z argumentów wartościowych.
Wiele parametrów typu z zależnościami między constraintami typescript
function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return { ...a, ...b };
}

const merged = merge({ id: 1 }, { name: 'Ada' });
// typ merged: { id: number } & { name: string }
Pułapka
Nadużywanie generyków tam, gdzie wystarczyłby union type lub konkretny typ, to over-engineering: jeśli funkcja realnie działa tylko na string | number, generyk <T> nie dodaje bezpieczeństwa, tylko iluzję elastyczności i gorszy komunikat błędu.
  • extends ogranicza dozwolone typy, nie deklaruje dziedziczenia jak w klasach.
  • keyof T + T[K] to standardowy wzorzec bezpiecznego dostępu do właściwości generycznych.
  • const type parameter eliminuje potrzebę 'as const' w wywołaniu, gdy funkcja sama chce zachować literal types.
  • Generyk bez co najmniej dwóch miejsc użycia tego samego typu w sygnaturze zwykle nie jest potrzebny.

6 · Typy warunkowe, infer i mapped types z key remapping

★ egzamin

Conditional types (T extends U ? X : Y) i mapped types ({ [K in keyof T]: ... }) to mechanizm meta-programowania na poziomie typów, pozwalający wyprowadzać nowe typy z istniejących zamiast pisać je ręcznie.

Termin kluczowy
Conditional type - Typ postaci 'T extends U ? X : Y', wybierający gałąź na podstawie relacji przypisywalności T do U. Rozdziela się (distributes) po unii, gdy T jest 'nagim' parametrem typu.
Termin kluczowy
infer - Słowo kluczowe używane wyłącznie wewnątrz warunku conditional type, deklarujące nową zmienną typu wywnioskowaną ze struktury sprawdzanego typu, np. wyciągnięcie typu zwracanego funkcji.
infer do wyciągania typu elementu z tablicy i zwracanej wartości funkcji typescript
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[]>; // string
type B = ElementType<number>;   // never

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type C = MyReturnType<() => Promise<number>>; // Promise<number>
Distributive conditional type po unii typescript
type ToArray<T> = T extends unknown ? T[] : never;
type D = ToArray<string | number>; // string[] | number[], nie (string | number)[]

// Zablokowanie dystrybucji: opakowanie T w krotkę wyłącza rozdzielanie po unii
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type E = ToArrayNonDist<string | number>; // (string | number)[]
Termin kluczowy
Mapped type - Typ generowany przez iterację po kluczach innego typu: '{ [K in keyof T]: Transform<T[K]> }'. Pozwala jednym wzorcem zdefiniować transformacje jak Readonly, Partial czy Record.
Termin kluczowy
Key remapping (klauzula 'as', TS 4.1+) - W mapped type klauzula 'as' po 'K in keyof T' pozwala zmienić lub odfiltrować klucze wynikowego typu, np. dodać prefiks albo usunąć klucz zwracając 'never' jako nowy klucz.
Key remapping: prefiksowanie i filtrowanie kluczy typescript
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person { name: string; age: number }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

// Filtrowanie: klucz zmapowany na 'never' znika z wynikowego typu
type OmitByValue<T, V> = {
  [K in keyof T as T[K] extends V ? never : K]: T[K];
};
type WithoutStrings = OmitByValue<{ a: string; b: number; c: boolean }, string>;
// { b: number; c: boolean }
Pułapka
infer wewnątrz warunku bez ograniczenia (bez extends na infer U) daje najszerszy możliwy typ, co bywa zbyt luźne; dodanie 'infer U extends SomeBase' (TS 4.7+) pozwala zawęzić już na etapie wnioskowania, nie dopiero po.
  • Conditional types rozdzielają się po unii tylko gdy sprawdzany typ to 'goły' parametr typu, nie opakowany w krotkę/obiekt.
  • infer działa wyłącznie w gałęzi 'extends' warunku, nie poza nim.
  • Klauzula 'as' w mapped type remapuje LUB usuwa klucze (zwrot 'never' = usunięcie).
  • To fundament większości utility types z lib.es5.d.ts i bibliotek typu Zod czy tRPC.

7 · Wbudowane utility types i operator satisfies

★ egzamin

TypeScript dostarcza zestaw generycznych utility types zbudowanych na mapped i conditional types (Partial, Pick, Omit, Record, ReturnType, Awaited), oraz operator 'satisfies' (od 4.9) łączący walidację typu z zachowaniem najwęższego wywnioskowanego typu.

Utility typeSygnatura (uproszczona)Zastosowanie
Partial<T>{ [K in keyof T]?: T[K] }Wszystkie pola opcjonalne, np. obiekt patch/update
Pick<T, K>{ [P in K]: T[P] }Wybór podzbioru pól istniejącego typu
Omit<T, K>Pick<T, Exclude<keyof T, K>>Typ bez wskazanych pól
Record<K, V>{ [P in K]: V }Mapa/słownik o znanych kluczach i jednolitym typie wartości
ReturnType<F>F extends (...a: any[]) => infer R ? R : neverWyciągnięcie typu zwracanego przez funkcję
Awaited<T>Rekurencyjnie odpakowuje Promise<Promise<...<T>>>Typ wartości po await, w tym zagnieżdżone Promise/thenable
Partial, Pick, Omit, Record w praktyce typescript
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

type UserPatch = Partial<Pick<User, 'name' | 'email'>>;
// { name?: string; email?: string }

type PublicUser = Omit<User, 'email'>;
// { id: string; name: string; role: 'admin' | 'user' }

type UsersByRole = Record<User['role'], User[]>;
// { admin: User[]; user: User[] }
ReturnType i Awaited na funkcji asynchronicznej typescript
async function fetchUser(id: string) {
  return { id, name: 'Ada' };
}

type FetchUserReturn = ReturnType<typeof fetchUser>; // Promise<{ id: string; name: string }>
type FetchUserResult = Awaited<FetchUserReturn>;     // { id: string; name: string }
Termin kluczowy
satisfies (TS 4.9+) - Operator walidujący, że wyrażenie pasuje do wskazanego typu, bez zmiany wywnioskowanego typu tego wyrażenia na szerszy. W przeciwieństwie do adnotacji ': Type', satisfies zachowuje najwęższy (literalny) typ.
satisfies vs adnotacja typu: różnica w wywnioskowanym typie typescript
type Colors = Record<string, string | [number, number, number]>;

// Adnotacja: traci precyzję, 'red' ma typ string | [number,number,number]
const palette1: Colors = { red: [255, 0, 0], green: '#00ff00' };
// palette1.red.toUpperCase(); // Error: 'toUpperCase' nie istnieje na typie [number,number,number], wymaga zawężenia unii

// satisfies: waliduje kształt, ale zachowuje precyzyjny literal type
const palette2 = { red: [255, 0, 0], green: '#00ff00' } satisfies Colors;
// palette2.red ma typ [number, number, number] bez zawężania, dostępne np. palette2.red[0]
Pułapka
'as Type' to asercja: wyłącza sprawdzanie i mówi compilerowi 'zaufaj mi', nie waliduje niczego i pozwala błędnie rzutować niekompatybilne typy (z ograniczeniami). 'satisfies Type' faktycznie sprawdza zgodność i zgłasza błąd przy niezgodności, jednocześnie nie zmieniając wywnioskowanego typu. To różne narzędzia, nie synonimy.
  • Używaj satisfies, gdy chcesz walidację kształtu bez utraty precyzji literalnych typów (np. konfiguracje, palety, routing tables).
  • Adnotacja ': Type' jest właściwa, gdy chcesz, by zmienna miała dokładnie ten szerszy typ, np. w publicznym API.
  • Awaited jest rekurencyjny, bo Promise<Promise<T>> w JS spłaszcza się przez await do T, typ musi to odzwierciedlać.

8 · Enumy, pułapki, strictNullChecks i template literal types

Enumy to jedyna konstrukcja TS generująca realny kod JS o nietrywialnym runtime behaviour, co czyni je źródłem częstych pułapek. strictNullChecks i template literal types dopełniają obraz nowoczesnego, precyzyjnego typowania.

Termin kluczowy
Numeric enum - Domyślny typ enum, kompilowany do obiektu z podwójnym mapowaniem (reverse mapping): nazwa→wartość i wartość→nazwa. Członkowie bez inicjalizatora auto-inkrementują się od 0.
Termin kluczowy
const enum - Enum całkowicie usuwany podczas kompilacji, wartości są inline'owane w miejscu użycia. Szybszy i bez runtime overhead. Zwykły const enum, zdefiniowany i używany w obrębie kompilowanego programu, działa poprawnie z --isolatedModules bez dodatkowej konfiguracji. Ograniczenie dotyczy wyłącznie ambient const enum (deklarowany bez implementacji, np. z zewnętrznego pliku .d.ts): odwołanie się do niego pod --isolatedModules nadal daje błąd TS2748, niezależnie od wersji TypeScript.
Pułapka numeric enum: number-typed zmienna omija kontrolę typów typescript
enum Direction {
  Up,
  Down,
  Left,
  Right,
}

function move(dir: Direction) {
  console.log(dir);
}

move(0);    // OK, Direction.Up, ale brak semantycznej gwarancji
// move(99); // Error TS2345 od TS 5.0: literal poza zakresem enuma jest odrzucany

declare const n: number;
move(n);    // OK w compilerze! zmienna typu 'number' nadal omija kontrolę enuma
Pułapka
Od TS 5.0 (przebudowa 'all enums are union enums') kompilator odrzuca literał liczbowy spoza zdefiniowanych wartości przekazany bezpośrednio do parametru typu enum, błąd TS2345, to poprawka względem TS 4.9 i starszych, gdzie taki literal przechodził bez ostrzeżenia. Realna pułapka, która przetrwała: zmienna typu zwykłego 'number' (nie literal) nadal jest swobodnie przypisywalna do typu enum bez żadnego zawężenia. String enum nie ma tego problemu, bo nie jest kompatybilny z 'string' w drugą stronę.
String enum lub union typu literałów jako bezpieczniejsza alternatywa typescript
type Direction2 = 'up' | 'down' | 'left' | 'right';

function move2(dir: Direction2) {
  console.log(dir);
}

move2('up');    // OK
// move2('diagonal'); // Error: nie jest dozwolonym literałem
// move2(0);          // Error: number nie jest Direction2
Rekomendacja społeczności i zespołu TS
Wiele zespołów (w tym część core teamu TS) rekomenduje union typów string-literal zamiast enum dla nowego kodu: brak runtime overhead, brak reverse-mapping pułapek, lepsza kompatybilność z --isolatedModules i narzędziami takimi jak esbuild/swc transpilującymi plik po pliku.
Termin kluczowy
strictNullChecks - Flaga (włączona w 'strict': true) rozdzielająca null/undefined od pozostałych typów: 'string' nie przyjmuje null, trzeba jawnie napisać 'string | null'. Bez niej null/undefined są cichymi członkami każdego typu, co jest głównym źródłem 'Cannot read property of undefined' w runtime.
Template literal types: budowanie typów stringowych ze wzorca typescript
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiRoute = `/api/${string}`;

type Endpoint = `${HttpMethod} ${ApiRoute}`;

const e1: Endpoint = 'GET /api/users';    // OK
// const e2: Endpoint = 'FETCH /api/users'; // Error: 'FETCH' nie jest HttpMethod

// Kombinacja z key remapping z sekcji 6: automatyczne generowanie nazw eventów
type EventName<T extends string> = `on${Capitalize<T>}Change`;
type ClickEvent = EventName<'click'>; // "onClickChange"
  • Preferuj string literal unions nad enum dla nowego kodu, chyba że potrzebujesz realnych wartości runtime i reverse mapping.
  • const enum eliminuje runtime overhead dla zwykłych enumów; ambient const enum pozostaje niekompatybilny z --isolatedModules (TS2748), niezależnie od wersji.
  • strictNullChecks powinno być włączone w każdym nowym projekcie, wyłączenie to prawie zawsze błąd konfiguracyjny, nie świadomy wybór.
  • Template literal types pozwalają walidować formaty stringów (route'y, nazwy CSS custom properties, eventy) na poziomie typów.

9 · Standardowe dekoratory (5.0+) i module resolution bundler

TypeScript 5.0 zastąpił eksperymentalne (legacy) dekoratory implementacją zgodną z finalizowaną propozycją TC39 Stage 3, dostępną bez flagi 'experimentalDecorators'. Ta sama wersja wprowadziła też tryb rozwiązywania modułów 'bundler' dopasowany do realnego zachowania Vite/esbuild/webpack.

Termin kluczowy
Standard decorators (TC39, stabilne w TS 5.0+) - Nowa składnia dekoratorów zgodna ze standardem ECMAScript, niezależna od flagi kompilatora. Dekorator to funkcja przyjmująca dekorowaną wartość i obiekt 'context' opisujący jej kind, name i inne metadane, zwracająca opcjonalnie zamiennik.
Standard decorator na metodzie klasy (bez experimentalDecorators) typescript
function logCall(target: Function, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);
  return function (this: unknown, ...args: unknown[]) {
    console.log(`Calling ${methodName} with`, args);
    return target.apply(this, args);
  };
}

class PaymentService {
  @logCall
  charge(amount: number) {
    return amount * 1.0;
  }
}

new PaymentService().charge(100); // loguje wywołanie przed uruchomieniem metody
Pułapka
Legacy dekoratory (experimentalDecorators: true, używane m.in. przez starszy Angular/NestJS/TypeORM) mają inną sygnaturę i semantykę niż standard decorators z TS 5.0. Nie są binarnie kompatybilne, migracja wymaga świadomej zmiany, nie jest to automatyczne 'flip flagi'.
Zgodność frameworków (stan na moment wydania TS 5.x)
Wiele frameworków (Angular, część ekosystemu NestJS/TypeORM) w praktyce nadal opiera się na legacy decorators przez metadata reflection (emitDecoratorMetadata), którego standard decorators formalnie nie definiują tak samo. Przed migracją projektu sprawdź wymagania konkretnego frameworka w jego aktualnej dokumentacji.
Termin kluczowy
moduleResolution: bundler (TS 5.0+) - Tryb rozwiązywania modułów naśladujący zachowanie nowoczesnych bundlerów (Vite, esbuild, webpack): rozumie 'exports' field w package.json bez wymuszania rozszerzeń plików w importach ESM, w odróżnieniu od trybu 'node16'/'nodenext'.
tsconfig.json: konfiguracja typowa dla projektu budowanego przez Vite/bundler json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "verbatimModuleSyntax": true,
    "noEmit": true
  }
}
Pułapka
moduleResolution: 'bundler' zakłada, że finalny bundling robi zewnętrzne narzędzie (Vite/webpack/esbuild), a nie 'tsc'. Użycie go razem z emitowaniem plików JS bezpośrednio przez tsc do uruchomienia w Node.js bez bundlera prowadzi do runtime błędów resolution, ten tryb nie jest przeznaczony do budowania paczek npm publikowanych bez bundlera.
  • Standard decorators nie wymagają już 'experimentalDecorators: true', są częścią samego języka od 5.0.
  • Sygnatura standard decoratora to (value, context), nie (target, propertyKey, descriptor) jak w legacy.
  • moduleResolution 'bundler' pasuje do projektów Vite/esbuild/webpack; 'node16'/'nodenext' pasuje do publikowanych paczek Node.js z natywnym ESM.
  • Framework-specific decorators (Angular, NestJS) mogą wciąż wymagać legacy trybu, sprawdź dokumentację frameworka przed migracją.
Fiszki - aktywne przypominanie

Sprawdź się - testowanie to nauka

24 pytań w losowej kolejności. Twoje wyniki zapisują się lokalnie.

Rozpocznij quiz
Zbudowane przez Tenzan Logic