TypeScript

TypeScript Decorators mit Claude Code: Metaprogramming 2026

📅 6. Mai 2026 ⏰ 11 min Lesezeit 📝 von SpockyMagicAI Team

TypeScript Decorators gehören zu den mächtigsten — und gleichzeitig am meisten missverstandenen — Features der Sprache. Mit dem TC39 Stage 3 Standard (seit TypeScript 5.0) hat sich die Syntax stabilisiert, während Frameworks wie NestJS, Angular und TypeORM weiter auf das Legacy-Modell (experimentalDecorators) setzen. Claude Code versteht beide Welten und hilft dir, Metaprogramming-Patterns sicher und produktiv einzusetzen.

📚 Inhaltsverzeichnis

  1. Decorator Grundlagen — TC39 Stage 3 vs. Legacy
  2. Class Decorators — Klassen dynamisch erweitern
  3. Method & Accessor Decorators — Verhalten kapseln
  4. Property & Parameter Decorators — Validation & DI
  5. reflect-metadata — Design-Time Typen nutzen
  6. NestJS Patterns — Decorators in der Praxis
TypeScript 5.x TC39 Stage 3 experimentalDecorators reflect-metadata NestJS Metaprogramming Claude Code DI Pattern

1. Decorator Grundlagen — TC39 Stage 3 vs. Legacy

Decorators sind spezielle Funktionen, die zur Laufzeit auf Klassen, Methoden, Accessors, Properties oder Parameter angewendet werden. Sie ermöglichen es, Verhalten deklarativ hinzuzufügen, ohne die eigentliche Implementierung zu verändern.

⚠ Zwei Modi in TypeScript 2026

Legacy (experimentalDecorators: true): Aktiv in NestJS, Angular, TypeORM. Stabil, aber nicht TC39-konform.

TC39 Stage 3 (Standard): Ab TypeScript 5.0, ohne extra tsconfig-Flag. Andere API, inkompatibel mit Legacy.

tsconfig.json — Die entscheidende Weiche

// tsconfig.json — Legacy-Modus (NestJS, Angular) { "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true, // für reflect-metadata "target": "ES2022", "lib": ["ES2022", "DOM"] } } // tsconfig.json — TC39 Standard-Modus (kein Flag nötig!) { "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM"] // experimentalDecorators NICHT setzen } }

Die @-Syntax — Grundprinzip

// Decorator = Funktion, die zur Laufzeit aufgerufen wird function meinDecorator(target: any) { console.log('Decorator aufgerufen für:', target.name); } @meinDecorator class MeineKlasse { // Decorator wird beim Laden der Klasse ausgeführt // → Ausgabe: "Decorator aufgerufen für: MeineKlasse" } // Decorator Factory — gibt Decorator zurück (für Parameter) function mitKonfiguration(options: { debug: boolean }) { return function(target: any) { if (options.debug) { console.log(`[DEBUG] Klasse geladen: ${target.name}`); } }; } @mitKonfiguration({ debug: true }) class MeineKlasse {}
🏭

Class Decorator

Erweitert oder ersetzt die Klasse selbst. Ideal für Singleton, Logging, Serialization.

Method Decorator

Wrapping von Methoden. Gut für Timing, Retry, Authorization, Caching.

📋

Property Decorator

Metadata für Properties definieren. Basis für Validation und DI-Systeme.

🔑

Parameter Decorator

Parameter-Metadata speichern. Fundamental für Dependency Injection.

2. Class Decorators — Klassen dynamisch erweitern

Class Decorators erhalten die Konstruktor-Funktion der Klasse und können sie entweder modifizieren oder vollständig ersetzen. Claude Code generiert sie zuverlässig mit korrekter TypeScript-Typisierung — ohne den häufigen Fehler, den Rückgabetyp falsch zu setzen.

Singleton Pattern

Class Decorator Design Pattern
// singleton.decorator.ts function Singleton<T extends { new(...args: any[]): any }>( constructor: T ): T { let instance: InstanceType<T> | undefined; return class extends constructor { constructor(...args: any[]) { if (instance) { return instance; } super(...args); instance = this as InstanceType<T>; } } as T; } @Singleton class DatabaseConnection { private connectionId: string; constructor() { this.connectionId = Math.random().toString(36); console.log(`DB Connection ${this.connectionId} erstellt`); } public query(sql: string): void { console.log(`[${this.connectionId}] Query: ${sql}`); } } const db1 = new DatabaseConnection(); // → erstellt Connection const db2 = new DatabaseConnection(); // → selbe Instanz! console.log(db1 === db2); // true

Logging Decorator — Klassen-Lifecycle tracken

// logging.decorator.ts function Loggable(prefix = '') { return function<T extends { new(...args: any[]): any }>( constructor: T ) { return class extends constructor { constructor(...args: any[]) { console.log(`[${prefix || constructor.name}] Instanz erstellt`, args); super(...args); } }; }; } interface Serializable { toJSON(): string; toObject(): Record<string, unknown>; } // Serialization Decorator — fügt Methoden hinzu function Serializable<T extends { new(...args: any[]): any }>( constructor: T ) { return class extends constructor { toJSON(): string { return JSON.stringify(this.toObject()); } toObject(): Record<string, unknown> { const obj: Record<string, unknown> = {}; for (const key of Object.keys(this)) { obj[key] = (this as any)[key]; } return obj; } }; } @Loggable('UserService') @Serializable class Benutzer { constructor( public name: string, public email: string ) {} } const user = new Benutzer('Anna', 'anna@example.com'); console.log((user as any).toJSON()); // → '{"name":"Anna","email":"anna@example.com"}'

💡 Claude Code Tipp: Decorator-Reihenfolge

Decorators werden von unten nach oben ausgeführt (bottom-up). Bei @Loggable @Serializable class X wird zuerst @Serializable, dann @Loggable angewendet. Claude Code warnt dich automatisch wenn die Reihenfolge semantisch wichtig ist.

3. Method & Accessor Decorators — Verhalten kapseln

Method Decorators erhalten drei Argumente: das Zielobjekt (target), den Methodennamen (propertyKey) und den Property Descriptor. Sie können den Descriptor modifizieren — und damit das Verhalten der Methode vollständig steuern.

Timing Decorator — Performance messen

Method Decorator Performance
// timing.decorator.ts function Timing(label?: string) { return function( target: any, propertyKey: string, descriptor: PropertyDescriptor ): PropertyDescriptor { const original = descriptor.value; const methodLabel = label ?? `${target.constructor.name}.${propertyKey}`; descriptor.value = async function(...args: any[]) { const start = performance.now(); try { const result = await original.apply(this, args); const ms = (performance.now() - start).toFixed(2); console.log(`⏱ [${methodLabel}] ${ms}ms`); return result; } catch (err) { const ms = (performance.now() - start).toFixed(2); console.error(`❌ [${methodLabel}] FEHLER nach ${ms}ms`, err); throw err; } }; return descriptor; }; } class UserRepository { @Timing('DB.findUser') async findById(id: number): Promise<string> { await new Promise(r => setTimeout(r, 120)); // DB-Abfrage simulieren return `User#${id}`; } } const repo = new UserRepository(); await repo.findById(42); // → ⏱ [DB.findUser] 122.34ms

Memoization Decorator — Ergebnisse cachen

// memoize.decorator.ts function Memoize(ttlMs = 5000) { return function( target: any, propertyKey: string, descriptor: PropertyDescriptor ): PropertyDescriptor { const cache = new Map<string, { value: any; expiresAt: number }>(); const original = descriptor.value; descriptor.value = async function(...args: any[]) { const key = JSON.stringify(args); const cached = cache.get(key); if (cached && Date.now() < cached.expiresAt) { console.log(`💾 [${propertyKey}] Cache-Hit für:`, key); return cached.value; } const result = await original.apply(this, args); cache.set(key, { value: result, expiresAt: Date.now() + ttlMs }); return result; }; return descriptor; }; } // Retry Decorator — automatische Wiederholungsversuche function Retry(maxAttempts = 3, delayMs = 1000) { return function( target: any, propertyKey: string, descriptor: PropertyDescriptor ): PropertyDescriptor { const original = descriptor.value; descriptor.value = async function(...args: any[]) { let lastError: unknown; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await original.apply(this, args); } catch (err) { lastError = err; console.warn(`[${propertyKey}] Versuch ${attempt}/${maxAttempts} fehlgeschlagen`); if (attempt < maxAttempts) { await new Promise(r => setTimeout(r, delayMs * attempt)); } } } throw lastError; }; return descriptor; }; } class ApiService { @Memoize(30000) // 30 Sekunden Cache @Retry(3, 500) // 3 Versuche, 500ms Delay async fetchUserProfile(userId: number) { const res = await fetch(`/api/users/${userId}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } }

Authorization Decorator — Zugriffsschutz

Method Decorator Security
// auth.decorator.ts type Role = 'admin' | 'editor' | 'viewer'; interface AuthContext { currentUser?: { roles: Role[] }; } function RequireRole(...requiredRoles: Role[]) { return function( target: any, propertyKey: string, descriptor: PropertyDescriptor ): PropertyDescriptor { const original = descriptor.value; descriptor.value = function( this: AuthContext, ...args: any[] ) { const user = this.currentUser; if (!user) { throw new Error('Nicht authentifiziert'); } const hasRole = requiredRoles.some(role => user.roles.includes(role) ); if (!hasRole) { throw new Error( `Zugriff verweigert. Erforderlich: ${requiredRoles.join(', ')}` ); } return original.apply(this, args); }; return descriptor; }; } class AdminController { currentUser = { roles: ['admin'] as Role[] }; @RequireRole('admin') deleteUser(id: number): void { console.log(`User ${id} gelöscht`); } @RequireRole('editor', 'admin') publishPost(postId: number): void { console.log(`Post ${postId} veröffentlicht`); } }

4. Property & Parameter Decorators — Validation & DI

Property Decorators laufen beim Klassenaufbau und ermöglichen es, Metadata über Properties zu speichern. Parameter Decorators sind das Herzstück jedes Dependency-Injection-Systems — sie teilen dem DI-Container mit, welche Abhängigkeiten eine Klasse benötigt.

Property Decorator — Validation

Property Decorator Validation
// validate.decorator.ts (Legacy experimentalDecorators) import 'reflect-metadata'; const VALIDATION_METADATA_KEY = Symbol('validation'); interface ValidationRule { property: string; type: 'required' | 'minLength' | 'maxLength' | 'pattern'; value?: any; message: string; } function Required(message = 'Pflichtfeld') { return function(target: any, propertyKey: string) { const rules: ValidationRule[] = Reflect.getMetadata(VALIDATION_METADATA_KEY, target) ?? []; rules.push({ property: propertyKey, type: 'required', message }); Reflect.defineMetadata(VALIDATION_METADATA_KEY, rules, target); }; } function MinLength(min: number, message?: string) { return function(target: any, propertyKey: string) { const rules: ValidationRule[] = Reflect.getMetadata(VALIDATION_METADATA_KEY, target) ?? []; rules.push({ property: propertyKey, type: 'minLength', value: min, message: message ?? `Mindestens ${min} Zeichen erforderlich` }); Reflect.defineMetadata(VALIDATION_METADATA_KEY, rules, target); }; } function validate<T extends object>(obj: T): string[] { const rules: ValidationRule[] = Reflect.getMetadata(VALIDATION_METADATA_KEY, obj) ?? []; const errors: string[] = []; for (const rule of rules) { const value = (obj as any)[rule.property]; switch (rule.type) { case 'required': if (!value) errors.push(rule.message); break; case 'minLength': if (String(value).length < rule.value) errors.push(rule.message); break; } } return errors; } class RegistrierungsFormular { @Required('Name ist ein Pflichtfeld') @MinLength(2) name: string = ''; @Required('E-Mail ist ein Pflichtfeld') email: string = ''; @MinLength(8, 'Passwort muss mindestens 8 Zeichen haben') passwort: string = ''; } const form = new RegistrierungsFormular(); form.name = 'A'; console.log(validate(form)); // → ['Mindestens 2 Zeichen erforderlich', 'E-Mail ist ein Pflichtfeld', ...]

Parameter Decorator — Dependency Injection

Parameter Decorator DI Pattern
// inject.decorator.ts import 'reflect-metadata'; const INJECT_TOKEN_KEY = Symbol('injectToken'); const container = new Map<symbol, any>(); // Token-basiertes DI function createToken<T>(description: string): symbol { return Symbol(description); } function provide<T>(token: symbol, value: T): void { container.set(token, value); } function Inject(token: symbol) { return function( target: any, propertyKey: string | undefined, parameterIndex: number ) { const tokens: Map<number, symbol> = Reflect.getMetadata(INJECT_TOKEN_KEY, target) ?? new Map(); tokens.set(parameterIndex, token); Reflect.defineMetadata(INJECT_TOKEN_KEY, tokens, target); }; } function Injectable<T extends { new(...args: any[]): any }>( constructor: T ): T { const tokens: Map<number, symbol> = Reflect.getMetadata(INJECT_TOKEN_KEY, constructor) ?? new Map(); return class extends constructor { constructor(...args: any[]) { const resolvedArgs = args.map((arg, i) => tokens.has(i) ? container.get(tokens.get(i)!) ?? arg : arg ); super(...resolvedArgs); } } as T; } // Verwendung const LOGGER_TOKEN = createToken<{ log: (msg: string) => void }>('Logger'); provide(LOGGER_TOKEN, { log: (msg: string) => console.log(`[LOG] ${msg}`) }); @Injectable class UserService { constructor( @Inject(LOGGER_TOKEN) private logger: { log: (msg: string) => void } ) {} createUser(name: string) { this.logger.log(`User erstellt: ${name}`); } }

5. reflect-metadata — Design-Time Typen nutzen

Das reflect-metadata-Paket ist die Brücke zwischen TypeScript-Typsystem und JavaScript-Laufzeit. Mit emitDecoratorMetadata: true in der tsconfig emittiert TypeScript automatisch Typ-Informationen, die Frameworks für automatische Dependency Injection nutzen können.

⚠ Installation erforderlich

npm install reflect-metadata
Dann im Einstiegspunkt: import 'reflect-metadata'; — VOR jedem anderen Import! Ohne emitDecoratorMetadata: true in der tsconfig gibt es keine Design-Time-Typen.

Reflect.metadata — Eigene Metadata speichern

// reflect-demo.ts import 'reflect-metadata'; // Eigene Metadata-Keys definieren const ROUTE_METADATA = Symbol('route'); const METHOD_METADATA = Symbol('httpMethod'); // Decorator mit Reflect.metadata function Route(path: string) { return Reflect.metadata(ROUTE_METADATA, path); } function Get(path = '/') { return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { Reflect.defineMetadata(METHOD_METADATA, { method: 'GET', path }, target, propertyKey); return descriptor; }; } @Route('/users') class UserController { @Get('/:id') findOne(id: string) { return { id }; } @Get('/') findAll() { return []; } } // Metadata zur Laufzeit lesen const baseRoute = Reflect.getMetadata(ROUTE_METADATA, UserController); console.log(`Base Route: ${baseRoute}`); // → /users const proto = UserController.prototype; const findOneRoute = Reflect.getMetadata(METHOD_METADATA, proto, 'findOne'); console.log(`Route: GET ${baseRoute}${findOneRoute.path}`); // → GET /users/:id

Design:type — Automatische Typ-Injektion

// Automatische Typ-Erkennung durch emitDecoratorMetadata import 'reflect-metadata'; function AutoInject() { return function<T extends { new(...args: any[]): any }>(constructor: T) { // TypeScript emittiert "design:paramtypes" automatisch const paramTypes: Function[] = Reflect.getMetadata('design:paramtypes', constructor) ?? []; console.log(`${constructor.name} benötigt:`); paramTypes.forEach((type, index) => { console.log(` Param ${index}: ${type.name}`); }); return constructor; }; } class LoggerService { log(msg: string) { console.log(msg); } } class ConfigService { get(key: string): string { return process.env[key] ?? ''; } } @AutoInject() class AppService { constructor( private logger: LoggerService, private config: ConfigService ) {} } // Ausgabe: // AppService benötigt: // Param 0: LoggerService // Param 1: ConfigService
Metadata Key Wert Verwendung
design:type Konstruktor des Property-Typs Property-Typ zur Laufzeit
design:paramtypes Array der Parameter-Typen DI-Container, Auto-Injection
design:returntype Rückgabetyp einer Methode Serialisierung, Validierung

6. NestJS Patterns — Decorators in der Praxis

NestJS ist das bekannteste TypeScript-Framework, das vollständig auf Decorators aufbaut. Fast jede Funktion — von Routing über Dependency Injection bis hin zu Guards und Pipes — wird über Decorators konfiguriert. Claude Code kennt alle NestJS-Decorators und generiert vollständig typisierte Module, Controller und Services.

@Injectable, @Controller, @Get — Das Basis-Trio

NestJS Class Decorator
// users/users.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private readonly userRepo: Repository<User> ) {} async findOne(id: number): Promise<User> { const user = await this.userRepo.findOne({ where: { id } }); if (!user) throw new NotFoundException(`User #${id} nicht gefunden`); return user; } async findAll(): Promise<User[]> { return this.userRepo.find(); } }
// users/users.controller.ts import { Controller, Get, Post, Body, Param, Delete, ParseIntPipe, HttpCode, HttpStatus, UseGuards } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; @Controller('users') @UseGuards(JwtAuthGuard) export class UsersController { constructor(private readonly usersService: UsersService) {} @Get() findAll() { return this.usersService.findAll(); } @Get(':id') findOne(@Param('id', ParseIntPipe) id: number) { return this.usersService.findOne(id); } @Post() @HttpCode(HttpStatus.CREATED) create(@Body() createUserDto: CreateUserDto) { return this.usersService.create(createUserDto); } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) remove(@Param('id', ParseIntPipe) id: number) { return this.usersService.remove(id); } }

Custom Decorators in NestJS — Eigene Abstraktion aufbauen

NestJS Custom Decorator
// decorators/current-user.decorator.ts import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { Request } from 'express'; interface AuthenticatedRequest extends Request { user: { id: number; email: string; roles: string[] }; } export const CurrentUser = createParamDecorator( (data: keyof AuthenticatedRequest['user'] | undefined, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest<AuthenticatedRequest>(); const user = request.user; return data ? user?.[data] : user; } ); // decorators/roles.decorator.ts import { SetMetadata } from '@nestjs/common'; export const ROLES_KEY = 'roles'; export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); // guards/roles.guard.ts import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(ctx: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [ ctx.getHandler(), ctx.getClass() ]); if (!requiredRoles?.length) return true; const { user } = ctx.switchToHttp().getRequest(); return requiredRoles.some(role => user?.roles?.includes(role)); } } // Verwendung im Controller @Controller('admin') @UseGuards(JwtAuthGuard, RolesGuard) export class AdminController { @Get('users') @Roles('admin') getAllUsers(@CurrentUser() user: any) { console.log(`Admin ${user.email} ruft alle User ab`); return []; } @Get('profile') getProfile( @CurrentUser('email') email: string, @CurrentUser('id') userId: number ) { return { email, userId }; } }
✓ Claude Code Best Practice

Wenn du Claude Code fragst, NestJS-Module zu generieren, erstellt es automatisch die korrekte Barrel-Struktur (users.module.ts, Controller, Service, Entity, DTO) mit vollständiger Typisierung — inklusive Swagger-Decorators (@ApiProperty, @ApiResponse) und Unit-Tests mit Jest.

NestJS Module — Alles zusammenführen

// users/users.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { User } from './user.entity'; import { RolesGuard } from '../auth/guards/roles.guard'; @Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UsersController], providers: [ UsersService, RolesGuard, ], exports: [UsersService], }) export class UsersModule {} // app.module.ts — Root Module @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forRootAsync({ useFactory: (config: ConfigService) => ({ type: 'postgres', url: config.getOrThrow<string>('DATABASE_URL'), entities: [__dirname + '/**/*.entity{.ts,.js}'], synchronize: config.get('NODE_ENV') !== 'production', }), inject: [ConfigService], }), UsersModule, AuthModule, ], }) export class AppModule {}

⚡ Decorator-Komposition in NestJS

NestJS bietet applyDecorators() um mehrere Decorators zu einem zu bündeln:

import { applyDecorators, UseGuards, HttpCode } from '@nestjs/common'; export function AdminOnly() { return applyDecorators( Roles('admin'), UseGuards(JwtAuthGuard, RolesGuard), ApiBearerAuth() ); } // Verwendung @Get('stats') @AdminOnly() getStats() { return {}; }

TypeScript Decorators mit KI-Unterstützung schreiben

Claude Code versteht beide Decorator-Systeme (TC39 und Legacy), generiert vollständige NestJS-Module und erklärt dir jeden Metaprogramming-Pattern — auf Deutsch, mit echten Beispielen.

Kostenlos ausprobieren →

Fazit: Metaprogramming mit Bedacht einsetzen

TypeScript Decorators sind ein mächtiges Werkzeug — aber kein Allheilmittel. Einige Leitlinien für den produktiven Einsatz:

Mit Claude Code als Pair-Programmer schreibst du Decorators, die nicht nur funktionieren, sondern auch typsicher, testbar und wartbar sind — egal ob du ein bestehendes NestJS-Projekt erweiterst oder ein neues TypeScript-Projekt von Grund auf aufbaust.

🔗 TypeScript 5.x 🔗 Stage 3 Decorators 🔗 NestJS 🔗 reflect-metadata 🔗 Dependency Injection 🔗 Metaprogramming