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:
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Complete guide to TypeScript 5 Complete Guide | Decorators, satisfies, const Type Parameters.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
Keywords Covered in This Article (Related Search Terms)
This article covers TypeScript, TypeScript 5, JavaScript, Frontend, Type Safety, Decorators.