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.
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}`);
}
}
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:
- Framework-Kompatibilität prüfen: NestJS/Angular/TypeORM benötigen Legacy-Modus (
experimentalDecorators: true). TC39-Standard für neue eigenständige Projekte.
- Keine Überdekorierung: Decorators erhöhen die Abstraktionsebene — zu viele Schichten machen Code schwer debuggbar.
- reflect-metadata nur mit emitDecoratorMetadata: Ohne dieses Flag sind keine Design-Time-Typen verfügbar.
- Testing beachten: Decorators verändern das Laufzeitverhalten — Unit-Tests müssen das berücksichtigen (Mocking der DI-Container).
- TC39-Zukunft: Der Stage-3-Standard wird Legacy langfristig ablösen — neue Projekte sollten ohne
experimentalDecorators starten.
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