TypeScript
System typów TypeScript 5.x: typowanie strukturalne, zawężanie, unie dyskryminowane, generyki, typy warunkowe, mapowane i szablonowe.
TypeScript
System typów TypeScript 5.x: typowanie strukturalne, zawężanie, unie dyskryminowane, generyki, typy warunkowe, mapowane i szablonowe.
Typowanie strukturalne i podstawy systemu typów
na egzaminieTypeScript 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.
Structural typing (duck typing): Dwa typy są kompatybilne, gdy mają ten sam kształt, czyli te same właściwości o kompatybilnych typach. Nazwa interfejsu czy klasy, którą deklarujesz, nie ma znaczenia.
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) 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ę.
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.
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.
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.
TypeScript pyta 'czy wygląda jak kaczka i kwacze jak kaczka', nie 'czy nazywa się Kaczka'.
any, unknown i never: trzy skrajne typy
na egzaminieany, unknown i never leżą na przeciwległych krańcach systemu typów. Typ any wyłącza kontrolę typów. Typ unknown wymusza jej odzyskanie przed użyciem. Typ never reprezentuje wartość, która nigdy nie powstanie.
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.
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).
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.
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
} 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
} | Typ | Przyjmuje dowolną wartość? | Operacje bez zawężenia | Typowy use case |
|---|---|---|---|
| any | Tak | Wszystkie, bez kontroli | Migracja z JS, biblioteki bez typów (unikać docelowo) |
| unknown | Tak | Brak, wymaga narrowing | Dane z zewnątrz: JSON.parse, odpowiedzi API, argumenty catch |
| never | Nie (zbiór pusty) | Brak, nic nie da się przypisać do niczego innego niż never | Exhaustiveness checking, funkcje zawsze rzucające |
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. To słuszne, bo w JS można rzucić cokolwiek, nie tylko Error.
any i unknown nie generują żadnego kodu runtime: to czysto kompilacyjne konstrukty. Typ never analogicznie znika po kompilacji do JS, ale wpływa na to, czy compiler pozwoli zbudować kod.
Zawężanie typów i user-defined type guards
na egzaminieNarrowing to proces, w którym compiler zawęża szeroki typ (unię) do węższego wewnątrz konkretnej gałęzi kodu. Opiera się na kontrolnym przepływie: typeof, instanceof, in, porównaniach równości lub własnych predykatach.
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.
User-defined type guard (predicate function): Funkcja zwracająca 'parameterName is Type' zamiast boolean. Informuje compiler, że pozytywny wynik funkcji zawęża typ argumentu do wskazanego typu we wszystkich miejscach wywołania.
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
}
} 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);
} 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.
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
} Type guard z błędną implementacją kłamie compilerowi, np. gdy sprawdza tylko 'code' in value, a nie typ message. TS ufa deklarowanemu predykatowi bez weryfikacji jego ciała. Błąd w guardzie nie daje żadnego błędu kompilacji, tylko crash w runtime później.
- Zdefiniuj predykat 'x is Type' zamiast zwracać boolean, gdy wynik ma zawężać typ dla wywołującego.
- Sprawdź każde pole używane dalej, guard musi realnie gwarantować kształt, nie tylko część.
- Preferuj discriminated union nad ręcznym type guard, gdy to możliwe, compiler zawęzi automatycznie po polu-discriminancie.
Discriminated unions i exhaustiveness checking
na egzaminieDiscriminated union (tagged union) to wzorzec, w którym każdy człon unii ma wspólne pole-literal (discriminant). Dzięki niemu compiler automatycznie zawęża typ w switch/if, bez ręcznych type guardów.
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.
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}`);
}
}
} Gałąź 'default' przypisująca 'shape' do zmiennej typu never kompiluje się tylko wtedy, gdy wszystkie warianty unii są obsłużone. Nowy wariant Shape bez obsługi w switch daje błąd kompilacji w tej linii: to najsilniejsza statyczna gwarancja kompletności.
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));
}
}
} Bez pola-discriminanta narrowing bywa niepełny lub wymaga ręcznych 'in' checks. Dotyczy to np. dwóch wariantów rozróżnianych tylko obecnością różnych pól, bez wspólnego tagu. Znacznie łatwiej wtedy o pominiętą gałąź, której compiler nie wykryje jako błędu.
- 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.
Discriminant + never w default = kompilator jako automatyczny reviewer kompletności switcha.
Generics: constraints, wnioskowanie, const type parameters
na egzaminieGeneryki 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.
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.
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 const type parameter (TS 5.0+): Modyfikator 'const' na parametrze typu (<const T>) wymusza wnioskowanie literalnego (najwęższego) typu argumentu zamiast rozszerzonego. Nie trzeba ręcznie pisać 'as const' w miejscu wywołania.
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 Podobnie jak parametry funkcji, parametry typu mogą mieć wartość domyślną: <T = string>. Jest używana, gdy caller nie poda argumentu typu i nie da się go wywnioskować z argumentów wartościowych.
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 } 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. Daje 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.
Typy warunkowe, infer i mapped types z key remapping
na egzaminieConditional 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.
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.
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.
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> 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)[] 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.
Key remapping (klauzula 'as', TS 4.1+): W mapped type klauzula 'as' po 'K in keyof T' pozwala zmienić lub odfiltrować klucze wynikowego typu. Można np. dodać prefiks albo usunąć klucz, zwracając 'never' jako nowy klucz.
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 } 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+) zawęża typ już na etapie wnioskowania, nie dopiero po nim.
- 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.
Wbudowane utility types i operator satisfies
na egzaminieTypeScript dostarcza zestaw generycznych utility types zbudowanych na mapped i conditional types (Partial, Pick, Omit, Record, ReturnType, Awaited). Operator 'satisfies' (od 4.9) łączy walidację typu z zachowaniem najwęższego wywnioskowanego typu.
| Utility type | Sygnatura (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 : never | Wycią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 |
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[] } 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 } 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.
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] '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, 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ć.
Enumy, pułapki, strictNullChecks i template literal types
Enumy to jedyna konstrukcja TS generująca realny kod JS o nietrywialnym runtime behaviour. Dlatego są źródłem częstych pułapek. strictNullChecks i template literal types dopełniają obraz nowoczesnego, precyzyjnego typowania.
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.
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, czyli deklarowanego 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.
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 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. Zgłasza wtedy błąd TS2345. W TS 4.9 i starszych 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ę.
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 Wiele zespołów (w tym część core teamu TS) rekomenduje dla nowego kodu union typów string-literal zamiast enum. Powody: brak runtime overhead, brak pułapek reverse-mapping, lepsza kompatybilność z --isolatedModules i narzędziami takimi jak esbuild/swc, transpilującymi plik po pliku.
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.
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.
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.
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.
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 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'.
Wiele frameworków (Angular, część ekosystemu NestJS/TypeORM) w praktyce nadal opiera się na legacy decorators przez metadata reflection (emitDecoratorMetadata). Standard decorators formalnie nie definiują metadata reflection tak samo. Przed migracją sprawdź wymagania konkretnego frameworka w jego aktualnej dokumentacji.
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 i nie wymusza rozszerzeń plików w importach ESM, w odróżnieniu od trybu 'node16'/'nodenext'.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"verbatimModuleSyntax": true,
"noEmit": true
}
} moduleResolution: 'bundler' zakłada, że finalny bundling robi zewnętrzne narzędzie (Vite/webpack/esbuild), a nie 'tsc'. Z nim JS emitowany przez tsc i uruchamiany w Node.js bez bundlera daje błędy resolution w runtime. Tryb nie służy 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ą.
Sprawdź się
20 z 24 pytań, losowo, z wyjaśnieniem po każdym. Wynik zostaje na tym urządzeniu.
Rozpocznij quiz