TypeScript 5 Complete Guide | Decorators· satisfies
이 글의 핵심
TypeScript 5 brings standard Decorators, the satisfies operator, const type parameters, and a 30% build speed improvement. This guide covers each feature with real-world examples you can apply immediately.
What Changed in TypeScript 5
TypeScript 5 (released March 2023) is a landmark release with three major language features and a significant performance improvement:
- Standard Decorators — ECMAScript-stage Decorators replace the decade-old
experimentalDecorators satisfiesoperator — validate type constraints while keeping precise inferred typesconsttype parameters — infer literal/tuple types instead of widened types- 30% faster builds — improved incremental compilation and module resolution
1. Standard Decorators
TypeScript 5 implements the TC39 Stage 3 Decorators proposal. The API is different from experimentalDecorators — decorators now receive a context object instead of operating on the prototype directly.
Class Decorator
// Logs every time the class is instantiated
function logged(value: Function, context: ClassDecoratorContext) {
const className = context.name;
return class extends value {
constructor(...args: any[]) {
super(...args);
console.log(`[${className}] Instance created`);
}
};
}
@logged
class User {
constructor(public name: string) {}
}
const user = new User('Alice');
// Output: [User] Instance created
Method Decorator — Measure Execution Time
function measure(target: Function, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
const start = performance.now();
const result = target.apply(this, args);
const end = performance.now();
console.log(`[${methodName}] ${(end - start).toFixed(2)}ms`);
return result;
};
}
class DataProcessor {
@measure
processLargeDataset(data: number[]): number {
return data.reduce((sum, n) => sum + n, 0);
}
}
const processor = new DataProcessor();
processor.processLargeDataset(Array.from({ length: 1_000_000 }, (_, i) => i));
// Output: [processLargeDataset] 5.23ms
Real-World Example: Route Registration
Build a lightweight HTTP router using decorators — similar to how NestJS works:
// Simple route registry
const routes = new Map<string, Function>();
function Route(path: string) {
return function (target: Function, context: ClassMethodDecoratorContext) {
routes.set(path, target);
return target;
};
}
class ApiController {
@Route('/api/users')
getUsers() {
return [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
}
@Route('/api/posts')
getPosts() {
return [{ id: 1, title: 'Hello TypeScript 5' }];
}
}
// Route dispatch
function handleRequest(path: string) {
const handler = routes.get(path);
return handler ? handler() : { error: 'Not Found' };
}
console.log(handleRequest('/api/users'));
// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
Migrating from experimentalDecorators
// TypeScript 4 (experimentalDecorators: true)
function OldDecorator(target: any, key: string, descriptor: PropertyDescriptor) {
// ...
return descriptor;
}
// TypeScript 5 (standard, no flag needed)
function NewDecorator(target: Function, context: ClassMethodDecoratorContext) {
// context.name = method name
// context.kind = 'method' | 'getter' | 'setter' | 'field' | 'accessor' | 'class'
// context.static = boolean
// context.private = boolean
return target;
}
Update tsconfig.json — remove experimentalDecorators:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler"
}
}
2. satisfies Operator
The satisfies operator validates that a value conforms to a type while preserving the original inferred type. It solves a common problem with as and type annotations.
The Problem satisfies Solves
type Color = 'red' | 'green' | 'blue' | [number, number, number];
// TypeScript 4 approach — annotate with the type
const palette: Record<string, Color> = {
primary: 'red',
secondary: [0, 255, 0],
};
// ❌ TypeScript loses precision — 'red' becomes Color, not 'red'
palette.primary.toUpperCase(); // Error: Property 'toUpperCase' does not exist on type 'Color'
palette.secondary[0]; // Error: 'secondary' might not be an array
// TypeScript 5 — use satisfies
const palette = {
primary: 'red',
secondary: [0, 255, 0],
} satisfies Record<string, Color>;
// ✅ Type check passes AND TypeScript keeps the precise types
palette.primary.toUpperCase(); // ✅ primary is still inferred as 'red' (a string)
palette.secondary[0]; // ✅ secondary is still [number, number, number]
Real-World Example: Configuration Object
type Environment = 'development' | 'staging' | 'production';
interface AppConfig {
env: Environment;
apiUrl: string;
features: Record<string, boolean>;
timeout: number;
}
// satisfies validates the shape while keeping literal types
const config = {
env: 'production',
apiUrl: 'https://api.example.com',
features: {
darkMode: true,
analyticsV2: false,
betaSearch: false,
},
timeout: 30_000,
} satisfies AppConfig;
// ✅ TypeScript knows config.env is literally 'production', not just Environment
if (config.env === 'production') {
enableProductionMonitoring();
}
// ✅ Feature flags are boolean, not unknown
console.log(config.features.darkMode); // boolean
satisfies vs as vs Type Annotation
| Type Annotation | as | satisfies | |
|---|---|---|---|
| Type checking | ✅ Yes | ❌ No (overrides) | ✅ Yes |
| Preserves inferred type | ❌ Widens to annotation | ❌ Widens to cast | ✅ Yes |
| Autocomplete precision | Lower | Lower | Higher |
| Use when | You want to enforce the type | You know better than TS | You want validation + precision |
3. const Type Parameters
Without const, generic functions widen literal types to their base types. const tells TypeScript to infer the narrowest possible type.
The Problem
// TypeScript 4
function makeArray<T>(items: T[]) {
return items;
}
const arr = makeArray([1, 2, 3]);
// Inferred type: number[] ← widened
// Desired type: readonly [1, 2, 3] ← literal tuple
The Solution
// TypeScript 5 — add const to the type parameter
function makeArray<const T>(items: T[]) {
return items;
}
const arr = makeArray([1, 2, 3]);
// Inferred type: readonly [1, 2, 3] ✅
Real-World Example: Type-Safe Route Config
function defineRoutes<const T extends Record<string, string>>(routes: T) {
return routes;
}
const routes = defineRoutes({
home: '/',
about: '/about',
contact: '/contact',
});
// TypeScript knows the exact route names
type RouteName = keyof typeof routes; // 'home' | 'about' | 'contact'
function navigate(route: RouteName) {
window.location.href = routes[route];
}
navigate('home'); // ✅
navigate('invalid'); // ❌ Type error at compile time
4. Performance Improvements
TypeScript 5 is 30% faster than TypeScript 4.9 for builds. Key improvements:
- Faster module resolution with the new
bundlermodule resolution mode - Improved incremental build caching
- Reduced redundant work during type checking
# TypeScript 4.9 (large project)
tsc --build # ~120 seconds
# TypeScript 5
tsc --build # ~84 seconds
New bundler Module Resolution
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler", // New in TS 5 — for Vite, webpack, esbuild
"allowImportingTsExtensions": true, // Import .ts files directly
"noEmit": true // Let your bundler handle output
}
}
5. Other New Features
All Enums are Union Enums
enum Status {
Pending = 'pending',
Active = 'active',
Inactive = 'inactive',
}
// TypeScript 5: enums behave like union types
type StatusLiteral = `${Status}`; // 'pending' | 'active' | 'inactive'
function processStatus(status: `${Status}`) {
// Works with both the enum member and string literals
}
processStatus(Status.Pending); // ✅
processStatus('pending'); // ✅
export type *
// types.ts
export type User = { id: number; name: string };
export type Post = { id: number; title: string };
export type Comment = { id: number; body: string };
// index.ts — re-export only types (no values)
export type * from './types';
Improved switch(true) Narrowing
function describe(value: string | number | boolean): string {
switch (true) {
case typeof value === 'string':
return value.toUpperCase(); // TypeScript 5: value is narrowed to string ✅
case typeof value === 'number':
return value.toFixed(2); // narrowed to number ✅
default:
return String(value);
}
}
Migration Guide: TypeScript 4 → 5
# 1. Update TypeScript
npm install -D typescript@latest
# 2. Check for errors
npx tsc --noEmit
# 3. Fix any new errors, then build
npm run build
Key tsconfig.json changes:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"experimentalDecorators": false,
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true
}
}
What to watch for:
- If you use
experimentalDecorators, keep it set totrueuntil your decorator libraries (NestJS, TypeORM) publish TS5-compatible updates - The new
bundlerresolution mode may catch previously-ignored import issues export type *is a new syntax — older TS versions won’t accept it
Summary
| Feature | What it solves | When to use |
|---|---|---|
| Standard Decorators | Replaces experimentalDecorators | Class-level metadata, AOP patterns |
satisfies | Validation without losing precision | Config objects, option maps |
const type params | Literal/tuple inference in generics | Route config, event registries |
bundler resolution | Works with Vite/webpack imports | Modern frontend projects |
Related posts:
- [Next.js App Router Rendering Guide](/en/blog/nextjs-app-router-rendering-strategies/
- [Git Beginner’s Guide](/en/blog/git-basic-usage-guide/
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Complete guide to TypeScript 5 Complete Guide | Decorators, satisfies, const Type Parameters. Learn with practical examp… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Next.js App Router에서 SSR·SSG·ISR 선택 가이드 | 렌더링 전략과 캐싱
- Git 기초 입문 [#1] — 설치·커밋·브랜치·원격 저장소 한 번에
이 글에서 다루는 키워드 (관련 검색어)
TypeScript, TypeScript 5, JavaScript, Frontend, Type Safety, Decorators 등으로 검색하시면 이 글이 도움이 됩니다.