装饰器与元数据反射
2026/7/9大约 6 分钟
装饰器与元数据反射
概述
装饰器(Decorator)是 TypeScript 中一种声明式元编程特性,允许对类、方法、属性和参数进行标注和修改。Go 没有装饰器的直接对应,但其 struct tag + 反射 机制提供了类似的元数据标注能力。对于 Go 开发者,可以将装饰器理解为"更强大的 struct tag",它不仅能附加元数据,还能修改行为。
Go 开发者已知
// Go struct tag —— 元数据标记
type User struct {
Name string `json:"name" validate:"required,min=3"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=150"`
}
// 运行时反射读取 tag
func validateUser(u interface{}) error {
t := reflect.TypeOf(u)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("validate")
// 解析 tag 并验证
fmt.Printf("Field %s: validate=%s\n", field.Name, tag)
}
return nil
}
// 方法注入(间接近似装饰器)
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Request: %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}Go 元编程特点:
- Struct tag 是编译期静态元数据(字符串)
- 反射在运行时读取 tag
- 没有修改行为的能力(只有标注)
- 中间件模式(函数包装)是 Go 的"装饰器"等效
- 强调显式而非隐式
TypeScript 怎么做
类装饰器
// 类装饰器
function LogClass<T extends { new (...args: any[]): object }>(target: T) {
return class extends target {
constructor(...args: any[]) {
super(...args)
console.log(`Class ${target.name} instantiated`)
}
}
}
@LogClass
class UserService {
constructor(private name: string) {}
}
const service = new UserService('Test')
// 输出: "Class UserService instantiated"方法装饰器
// 方法装饰器 —— 日志记录
function LogMethod(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value
descriptor.value = function (...args: unknown[]) {
console.log(`Calling ${propertyKey} with:`, args)
const result = original.apply(this, args)
console.log(`Result:`, result)
return result
}
return descriptor
}
class Calculator {
@LogMethod
add(a: number, b: number): number {
return a + b
}
}
const calc = new Calculator()
calc.add(2, 3)
// Calling add with: [2, 3]
// Result: 5属性装饰器
// 属性装饰器 —— 默认值
function DefaultValue(value: string) {
return function (target: object, propertyKey: string) {
let val = value
Object.defineProperty(target, propertyKey, {
get: () => val,
set: (v) => { val = v },
enumerable: true,
configurable: true,
})
}
}
class Config {
@DefaultValue('localhost')
host!: string
@DefaultValue('8080')
port!: string
}
const config = new Config()
console.log(config.host) // "localhost"参数装饰器
// 参数装饰器
function LogParameter(
target: object,
propertyKey: string,
parameterIndex: number
) {
console.log(
`Parameter at index ${parameterIndex} in ${propertyKey}`
)
}
class Logger {
log(@LogParameter message: string) {
console.log(message)
}
}装饰器工厂
// 装饰器工厂 —— 带参数的装饰器
function Log(level: 'info' | 'warn' | 'error') {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value
descriptor.value = function (...args: unknown[]) {
const prefix = {
info: '[INFO]',
warn: '[WARN]',
error: '[ERROR]',
}[level]
console.log(`${prefix} ${propertyKey}`, ...args)
return original.apply(this, args)
}
return descriptor
}
}
class Service {
@Log('info')
getUser(id: number) {
return { id, name: 'Alice' }
}
@Log('error')
deleteUser(id: number) {
throw new Error('Cannot delete')
}
}reflect-metadata
import 'reflect-metadata'
// 使用元数据装饰器
function Injectable(): ClassDecorator {
return (target) => {
// 注册依赖元数据
const params = Reflect.getMetadata('design:paramtypes', target)
console.log(`Injecting: ${params?.map((p: any) => p.name)}`)
Reflect.defineMetadata('injectable', true, target)
}
}
function Inject(token: string): ParameterDecorator {
return (target, propertyKey, parameterIndex) => {
const existing =
Reflect.getOwnMetadata('inject:params', target) ?? []
existing.push({ index: parameterIndex, token })
Reflect.defineMetadata('inject:params', existing, target)
}
}
@Injectable()
class DatabaseService {
constructor(private url: string) {}
}
// 简单的 DI 容器
class Container {
private static instances = new Map<string, object>()
static resolve<T>(target: new (...args: any[]) => T): T {
const isInjectable = Reflect.getMetadata('injectable', target)
if (!isInjectable) {
throw new Error(`${target.name} is not injectable`)
}
const params = Reflect.getMetadata('design:paramtypes', target) ?? []
const injections = params.map((param: any) => {
return this.resolve(param)
})
const instance = new target(...injections)
this.instances.set(target.name, instance)
return instance
}
}差异分析
| 维度 | Go | TypeScript |
|---|---|---|
| 元数据标注 | Struct tag(字符串) | 装饰器 + reflect-metadata |
| 行为修改 | 不可(函数包装实现) | 可(替换 descriptor) |
| 运行时反射 | reflect 包 | Reflect API |
| 静态分析 | 编译器不支持 | 支持(装饰器 AST 可见) |
| 声明式风格 | 有限(tag 字符串) | 强大的声明式语法 |
| 类型安全 | tag 是纯字符串,无验证 | 装饰器有类型签名 |
| AOP 支持 | 中间件链 | 装饰器组合 |
| 学习曲线 | 低(简单明了) | 中(需要了解 descriptor) |
Go struct tag vs TS 装饰器
// Go: 纯字符串标记,无类型安全
type User struct {
Name string `json:"name" validate:"required|min:3|max:100"`
}// TS: 可执行的元编程
class User {
@Validate({ required: true, minLength: 3, maxLength: 100 })
name!: string
}Go 的 tag 是被动的(仅存储字符串,需要解析器解释),而 TypeScript 的装饰器是主动的(可以在运行时修改行为)。
Bad Practice
// Bad: 装饰器中的副作用
function LogToFile(path: string) {
// 装饰器定义时立即写文件(副作用)
fs.appendFileSync(path, 'Decorator loaded\n')
return function (target: object, key: string) {
const original = (target as any)[key]
;(target as any)[key] = function (...args: unknown[]) {
// ...
}
}
}
// Bad: 装饰器过度依赖执行顺序
function First(target: any) {
console.log('First')
}
function Second(target: any) {
console.log('Second')
}
@First
@Second
class MyClass {} // 实际输出: Second, First(内层先执行)
// Bad: 装饰器修改了类型签名而不声明
function AddMethod(target: any) {
target.prototype.newMethod = () => {} // TS 不知道这个新方法
}
// Bad: 用装饰器替代简单的函数组合
class Service {
@Log
@Validate
@Cache
@Retry
@Timeout
fetchData() {} // 5 个装饰器!调试困难
}Best Practice
// Good: 优先函数组合(类似 Go 的中间件)
type Middleware = (fn: Function) => Function
function withLogging<T extends (...args: any[]) => any>(fn: T): T {
return ((...args: Parameters<T>) => {
console.log(`Calling ${fn.name}`)
return fn(...args)
}) as T
}
function withCaching<T extends (...args: any[]) => any>(fn: T): T {
const cache = new Map<string, ReturnType<T>>()
return ((...args: Parameters<T>) => {
const key = JSON.stringify(args)
if (cache.has(key)) return cache.get(key)!
const result = fn(...args)
cache.set(key, result)
return result
}) as T
}
// 函数组合
const enhancedFetch = withLogging(withCaching(fetchData))
// Good: 装饰器用于横切关注点(Cross-cutting Concerns)
function MeasureTime(): MethodDecorator {
return (target, key, descriptor) => {
const original = descriptor.value
descriptor.value = function (...args: unknown[]) {
const start = performance.now()
const result = original.apply(this, args)
const end = performance.now()
console.log(`${String(key)} took ${end - start}ms`)
return result
}
}
}
// Good: 使用装饰器实现依赖注入
const INJECTION_KEY = Symbol('injection')
function Inject(serviceIdentifier: string): ParameterDecorator {
return (target, _propertyKey, parameterIndex) => {
const existing: Map<number, string> =
Reflect.getOwnMetadata(INJECTION_KEY, target) ?? new Map()
existing.set(parameterIndex, serviceIdentifier)
Reflect.defineMetadata(INJECTION_KEY, existing, target)
}
}
// Good: decorator 工厂提供可配置性
function Retry(maxAttempts: number = 3, delay: number = 1000): MethodDecorator {
return (target, key, descriptor) => {
const original = descriptor.value
descriptor.value = async function (...args: unknown[]) {
for (let i = 0; i < maxAttempts; i++) {
try {
return await original.apply(this, args)
} catch (error) {
if (i === maxAttempts - 1) throw error
await new Promise(r => setTimeout(r, delay))
}
}
}
}
}
// 使用
class ApiClient {
@Retry(3, 2000)
async fetchData(): Promise<Data> {
const response = await fetch('/api/data')
return response.json()
}
}装饰器有副作用,务必保持纯粹
不推荐: 装饰器定义时执行副作用操作(写文件、网络请求、全局状态修改)
推荐: 装饰器只进行行为包装和元数据注册
// 正确做法:只在方法被调用时执行副作用
function AuditLog(): MethodDecorator {
return (target, key, descriptor) => {
const original = descriptor.value
descriptor.value = function (...args: unknown[]) {
console.log(`[AUDIT] ${String(key)} called`) // 运行时
return original.apply(this, args)
}
}
}code-tabs 对比
总结
| Go 概念 | TypeScript 对应 |
|---|---|
| Struct tag | 属性装饰器 + reflect-metadata |
反射 reflect.TypeOf | Reflect.getMetadata |
| HTTP 中间件 | 方法装饰器 / 函数组合 |
| 编译期序列化标签 | 装饰器工厂 |
| 无对应 | 类装饰器(构造函数替换) |
| 无对应 | 参数装饰器 |
| 无对应 | design:paramtypes 元数据 |
核心建议:
- 装饰器最适合横切关注点(日志、缓存、鉴权、度量、重试)
- 优先函数组合,需要时才用装饰器
- 装饰器应保持纯粹,避免副作用
reflect-metadata是 DI 容器的基石- Go 的显式风格 vs TS 的声明式风格 —— 没有绝对的优劣,选择适合场景的模式
装饰器是 TypeScript 最强大的元编程工具之一,但正如所有的元编程,力量越大,责任越大。克制使用,优先考虑显式的函数组合方案。