Experte162025-01-15

TypeScript Decorators - Metaprogrammierung

Lerne Decorators für Classes, Methods und Properties. Experimentelle Features für fortgeschrittene Patterns.

#typescript#decorators#advanced#metaprogramming

TypeScript Decorators - Metaprogrammierung

Decorators sind ein experimentelles Feature für Metaprogrammierung in TypeScript.

Was sind Decorators?

Decorators sind Funktionen die andere Code-Elemente zur Design-Time modifizieren können:

  • Classes
  • Methods
  • Properties
  • Parameters

⚠️ Experimentell: Decorators sind Stage 3 Proposal und benötigen:

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Class Decorators

function Component(target: Function) {
  console.log(`Component: ${target.name}`)
  target.prototype.isComponent = true
}

@Component
class MyComponent {
  name = "MyComponent"
}

const instance = new MyComponent()
console.log((instance as any).isComponent) // true

Mit Factory Pattern

function Component(options: { selector: string; template: string }) {
  return function (target: Function) {
    target.prototype.selector = options.selector
    target.prototype.template = options.template
  }
}

@Component({
  selector: 'app-user',
  template: '<div>User Component</div>'
})
class UserComponent {
  name = "UserComponent"
}

Constructor Replacement

function Singleton<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    static instance: any

    constructor(...args: any[]) {
      super(...args)
      if ((constructor as any).instance) {
        return (constructor as any).instance
      }
      (constructor as any).instance = this
    }
  }
}

@Singleton
class Database {
  constructor(public url: string) {}
}

const db1 = new Database("mongodb://localhost")
const db2 = new Database("postgresql://localhost")
console.log(db1 === db2) // true - gleiche Instanz

Method Decorators

function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with:`, args)
    const result = originalMethod.apply(this, args)
    console.log(`Result:`, result)
    return result
  }

  return descriptor
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b
  }
}

const calc = new Calculator()
calc.add(2, 3)
// Console:
// Calling add with: [2, 3]
// Result: 5

Performance Measurement

function Measure(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value

  descriptor.value = async function (...args: any[]) {
    const start = performance.now()
    const result = await originalMethod.apply(this, args)
    const end = performance.now()
    console.log(`${propertyKey} took ${(end - start).toFixed(2)}ms`)
    return result
  }

  return descriptor
}

class DataService {
  @Measure
  async fetchData(): Promise<any[]> {
    await new Promise(resolve => setTimeout(resolve, 1000))
    return [1, 2, 3]
  }
}

Memoization

function Memoize(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value
  const cache = new Map<string, any>()

  descriptor.value = function (...args: any[]) {
    const key = JSON.stringify(args)

    if (cache.has(key)) {
      console.log(`Cache hit for ${propertyKey}`)
      return cache.get(key)
    }

    const result = originalMethod.apply(this, args)
    cache.set(key, result)
    return result
  }

  return descriptor
}

class MathService {
  @Memoize
  fibonacci(n: number): number {
    if (n <= 1) return n
    return this.fibonacci(n - 1) + this.fibonacci(n - 2)
  }
}

Property Decorators

function Min(min: number) {
  return function (target: any, propertyKey: string) {
    let value: number

    const getter = () => value
    const setter = (newValue: number) => {
      if (newValue < min) {
        throw new Error(`${propertyKey} must be at least ${min}`)
      }
      value = newValue
    }

    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    })
  }
}

class Product {
  @Min(0)
  price!: number

  @Min(1)
  quantity!: number
}

const product = new Product()
product.price = 100 // ✅ OK
product.price = -10 // ❌ Error: price must be at least 0

Required Validation

function Required(target: any, propertyKey: string) {
  let value: any

  const getter = () => {
    if (value === undefined) {
      throw new Error(`${propertyKey} is required`)
    }
    return value
  }

  const setter = (newValue: any) => {
    if (newValue === undefined || newValue === null) {
      throw new Error(`${propertyKey} cannot be undefined or null`)
    }
    value = newValue
  }

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter
  })
}

class User {
  @Required
  email!: string

  @Required
  name!: string
}

Parameter Decorators

function Validate(
  target: any,
  propertyKey: string,
  parameterIndex: number
) {
  const existingValidators = Reflect.getMetadata(
    "validate",
    target,
    propertyKey
  ) || []

  existingValidators.push(parameterIndex)

  Reflect.defineMetadata(
    "validate",
    existingValidators,
    target,
    propertyKey
  )
}

class UserService {
  createUser(@Validate email: string, @Validate name: string) {
    // Validierung hier
  }
}

Decorator Composition

Mehrere Decorators kombinieren:

function First() {
  console.log("First(): factory evaluated")
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("First(): called")
  }
}

function Second() {
  console.log("Second(): factory evaluated")
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("Second(): called")
  }
}

class Example {
  @First()
  @Second()
  method() {}
}

// Output:
// First(): factory evaluated
// Second(): factory evaluated
// Second(): called
// First(): called

Reihenfolge:

  1. Factories von oben nach unten
  2. Decorators von unten nach oben
Real-World API Controller mit Decorators
// Decorators
function Controller(basePath: string) {
  return function (target: Function) {
    Reflect.defineMetadata("basePath", basePath, target)
  }
}

function Get(path: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    Reflect.defineMetadata("path", path, target, propertyKey)
    Reflect.defineMetadata("method", "GET", target, propertyKey)
  }
}

function Post(path: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    Reflect.defineMetadata("path", path, target, propertyKey)
    Reflect.defineMetadata("method", "POST", target, propertyKey)
  }
}

function Auth(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value

  descriptor.value = async function (...args: any[]) {
    const [req] = args
    if (!req.user) {
      throw new Error("Unauthorized")
    }
    return originalMethod.apply(this, args)
  }

  return descriptor
}

// Usage
@Controller("/api/users")
class UserController {
  @Get("/")
  getAllUsers(req: any, res: any) {
    return res.json({ users: [] })
  }

  @Get("/:id")
  getUserById(req: any, res: any) {
    return res.json({ user: { id: req.params.id } })
  }

  @Post("/")
  @Auth
  createUser(req: any, res: any) {
    return res.json({ user: req.body })
  }

  @Post("/:id")
  @Auth
  updateUser(req: any, res: any) {
    return res.json({ user: { id: req.params.id, ...req.body } })
  }
}

📝 Quiz

In welcher Reihenfolge werden gestapelte Decorators ausgeführt?

Tipps & Tricks

Reflect Metadata

npm install reflect-metadata
import "reflect-metadata"

function LogType(target: any, key: string) {
  const type = Reflect.getMetadata("design:type", target, key)
  console.log(`${key} type: ${type.name}`)
}

class User {
  @LogType
  name: string = ""

  @LogType
  age: number = 0
}
// Console: name type: String
// Console: age type: Number

NestJS Style Decorators

// Custom Injectable Decorator
const Injectable = (): ClassDecorator => {
  return (target) => {
    // Register in DI container
    Reflect.defineMetadata("injectable", true, target)
  }
}

@Injectable()
class UserService {
  getUsers() {
    return []
  }
}

Validation Decorators

function IsEmail(target: any, propertyKey: string) {
  let value: string

  const getter = () => value

  const setter = (newValue: string) => {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    if (!emailRegex.test(newValue)) {
      throw new Error("Invalid email format")
    }
    value = newValue
  }

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter
  })
}

class User {
  @IsEmail
  email!: string
}

Häufige Fehler

Fehler 1: experimentalDecorators nicht aktiviert

FEHLER:

error TS1219: Experimental support for decorators is a feature that is subject to change in a future release.

LÖSUNG:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

Fehler 2: this Context verlieren

FALSCH:

descriptor.value = function (...args: any[]) {
  // this ist verloren
}

RICHTIG:

descriptor.value = function (this: any, ...args: any[]) {
  const result = originalMethod.apply(this, args)
  return result
}

Fehler 3: Async Decorators nicht richtig handhaben

FALSCH:

descriptor.value = function (...args: any[]) {
  originalMethod.apply(this, args) // Verliert Promise
}

RICHTIG:

descriptor.value = async function (...args: any[]) {
  return await originalMethod.apply(this, args)
}
🎯

Zusammenfassung

Du hast gelernt:

  • ✅ Decorators für Classes, Methods, Properties
  • ✅ Factory Pattern für parametrisierte Decorators
  • ✅ Method Decorators für Logging, Caching
  • ✅ Property Decorators für Validation
  • ✅ Decorator Composition
  • ✅ Real-World Patterns (API Controllers)

Key Takeaways:

  • experimentalDecorators aktivieren
  • Decorators = Metaprogrammierung
  • Reihenfolge: unten nach oben
  • this Context beachten
  • Reflect Metadata für erweiterte Features

Frameworks mit Decorators:

  • NestJS (Backend)
  • Angular (Frontend)
  • TypeORM (Database)
  • TypeGraphQL (GraphQL)

Nächste Schritte

Decorators sind mächtig für:

  • Dependency Injection
  • Validation
  • Caching & Memoization
  • Logging & Monitoring
  • API Routing

Viel Erfolg! 🚀

TypeScriptLektion 14 von 15
93% abgeschlossen
Lektion abgeschlossen!

Gut gemacht! 🎉

Du hast "TypeScript Decorators - Metaprogrammierung" abgeschlossen

Artikel bewerten

0.0 (0 Bewertungen)

Bitte einloggen um zu bewerten