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:
- Factories von oben nach unten
- Decorators von unten nach oben
// 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! 🚀
Gut gemacht! 🎉
Du hast "TypeScript Decorators - Metaprogrammierung" abgeschlossen
Artikel bewerten
Bitte einloggen um zu bewerten
Das könnte dich auch interessieren
TypeScript Mapped Types - Type Transformationen
Lerne Mapped Types für dynamische Type-Transformationen. Conditional Types, Template Literals und Advanced Patterns.
TypeScript Generics - Wiederverwendbare Typen
Lerne Generics in TypeScript - das Werkzeug für flexible und wiederverwendbare Komponenten, Funktionen und Classes.
TypeScript Utility Types - Eingebaute Type Helpers
Lerne die wichtigsten eingebauten Utility Types: Partial, Required, Pick, Omit, Record und mehr.