본문으로 건너뛰기
Previous
Next
TypeScript 5 Complete Guide | Decorators· satisfies

TypeScript 5 Complete Guide | Decorators· satisfies

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:

  1. Standard Decorators — ECMAScript-stage Decorators replace the decade-old experimentalDecorators
  2. satisfies operator — validate type constraints while keeping precise inferred types
  3. const type parameters — infer literal/tuple types instead of widened types
  4. 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 Annotationassatisfies
Type checking✅ Yes❌ No (overrides)✅ Yes
Preserves inferred type❌ Widens to annotation❌ Widens to cast✅ Yes
Autocomplete precisionLowerLowerHigher
Use whenYou want to enforce the typeYou know better than TSYou 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 bundler module 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 to true until your decorator libraries (NestJS, TypeORM) publish TS5-compatible updates
  • The new bundler resolution mode may catch previously-ignored import issues
  • export type * is a new syntax — older TS versions won’t accept it

Summary

FeatureWhat it solvesWhen to use
Standard DecoratorsReplaces experimentalDecoratorsClass-level metadata, AOP patterns
satisfiesValidation without losing precisionConfig objects, option maps
const type paramsLiteral/tuple inference in genericsRoute config, event registries
bundler resolutionWorks with Vite/webpack importsModern 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와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

TypeScript, TypeScript 5, JavaScript, Frontend, Type Safety, Decorators 등으로 검색하시면 이 글이 도움이 됩니다.