TypeScript装饰器与元编程实战

TypeScript装饰器与元编程实战

作者:专注前端开发,分享工程化实战经验

更新时间:2026年5月

阅读时长:约15分钟


前言:为什么装饰器是TypeScript的杀手锏?

如果你使用过Angular或NestJS一定会注意到:它们的代码充满了@Injectable()、@Component()、@Controller()这样的"魔法标记"。这就是**装饰器(Decorators)**的魔力。

装饰器让TypeScript具备了元编程能力------用声明式的方式为类、方法、属性添加额外行为。本文将带你从入门到实战,彻底掌握这一强大特性。


一、装饰器基础入门

1.1 什么是装饰器?

装饰器是一个函数,它能在不修改原始类的前提下,给类或类的成员添加额外功能:

typescript 复制代码
// 简单装饰器
function Logger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with`, args);
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

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

const calc = new Calculator();
calc.add(1, 2);
// 输出: Calling add with [ 1, 2 ]
// 返回: 3

1.2 装饰器的四种类型

typescript 复制代码
// 1. 类装饰器
function Controller(path: string) {
  return function (target: new () => any) {
    console.log(`Registered controller at /${path}`);
  };
}

// 2. 方法装饰器
function Get(path: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    // 注册路由逻辑
  };
}

// 3. 属性装饰器
function Autowired(key: string) {
  return function (target: any, propertyKey: string) {
    // 依赖注入逻辑
  };
}

// 4. 参数装饰器
function Param(source: string) {
  return function (target: any, propertyKey: string, index: number) {
    // 参数处理逻辑
  };
}

二、类装饰器实战

2.1 路由控制器装饰器

模拟NestJS的路由装饰器:

typescript 复制代码
interface RouteHandler {
  path: string;
  method: string;
  handler: Function;
}

function Controller(basePath: string) {
  return function <T extends new (...args: any[]) => any>(target: T) {
    return class extends target {
      private routes: RouteHandler[] = [];
      
      constructor(...args: any[]) {
        super(...args);
        // 注册所有路由
        this.registerRoutes();
      }
      
      private registerRoutes() {
        // 收集元数据
        // ...
        console.log(`Controller ${target.name} mounted at /${basePath}`);
      }
    };
  };
}

function Get(path: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;
    descriptor.value = function (...args: any[]) {
      console.log(`GET /${path} handled by ${propertyKey}`);
      return original.apply(this, args);
    };
    return descriptor;
  };
}

// 使用
@Controller("users")
class UserController {
  @Get("list")
  findAll() {
    return [{ id: 1, name: "张三" }];
  }
  
  @Get(":id")
  findById(id: number) {
    return { id, name: "张三" };
  }
}

2.2 单例模式装饰器

typescript 复制代码
function Singleton(target: new () => any) {
  let instance: any = null;
  
  return new Proxy(target, {
    construct: function (cls, args) {
      if (!instance) {
        instance = new cls(...args);
      }
      return instance;
    }
  });
}

@Singleton
class ConfigService {
  private config = { apiUrl: "https://api.example.com" };
  
  get(key: string) {
    return (this.config as any)[key];
  }
}

const a = new ConfigService();
const b = new ConfigService();
console.log(a === b); // true

三、方法装饰器进阶

3.1 缓存装饰器

typescript 复制代码
function Cache(ttl: number = 60000) {
  const cache = new Map<string, { value: any; expires: number }>();
  
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;
    
    descriptor.value = function (...args: any[]) {
      const key = `${propertyKey}:${JSON.stringify(args)}`;
      const cached = cache.get(key);
      
      if (cached && cached.expires > Date.now()) {
        console.log(`Cache hit for ${propertyKey}`);
        return cached.value;
      }
      
      const result = original.apply(this, args);
      
      // 处理Promise
      if (result instanceof Promise) {
        return result.then((value: any) => {
          cache.set(key, { value, expires: Date.now() + ttl });
          return value;
        });
      }
      
      cache.set(key, { value: result, expires: Date.now() + ttl });
      return result;
    };
    
    return descriptor;
  };
}

class UserService {
  @Cache(5000) // 缓存5秒
  async getUser(id: number) {
    await new Promise(r => setTimeout(r, 100));
    return { id, name: "用户" + id };
  }
}

3.2 重试装饰器

typescript 复制代码
function Retry(
  maxAttempts: number = 3,
  delay: number = 1000
) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;
    
    descriptor.value = async function (...args: any[]) {
      let lastError: Error;
      
      for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
          return await original.apply(this, args);
        } catch (error) {
          lastError = error as Error;
          console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
          
          if (attempt < maxAttempts) {
            await new Promise(r => setTimeout(r, delay));
          }
        }
      }
      
      throw lastError!;
    };
    
    return descriptor;
  };
}

class ApiClient {
  @Retry(3, 500)
  async fetch(url: string) {
    if (Math.random() > 0.7) {
      throw new Error("Network error");
    }
    return { data: "success" };
  }
}

四、属性装饰器与依赖注入

4.1 简单的依赖注入容器

typescript 复制代码
type Constructor<T = any> = new (...args: any[]) => T;

// 服务容器
const Container = new Map<Constructor, Constructor>();

function Injectable(token?: string) {
  return function (target: Constructor) {
    const t = token || target;
    Container.set(t, target);
    console.log(`Registered service: ${target.name}`);
  };
}

function Inject(token: string) {
  return function (target: any, propertyKey: string) {
    // 替换属性为注入的服务实例
    Object.defineProperty(target, propertyKey, {
      get: () => {
        const ServiceClass = Container.get(token as any);
        return new ServiceClass();
      }
    });
  };
}

// 使用
@Injectable("UserService")
class UserService {
  getUsers() {
    return ["张三", "李四"];
  }
}

class UserController {
  @Inject("UserService")
  private userService!: UserService;
  
  list() {
    return this.userService.getUsers();
  }
}

五、装饰器工厂与组合

5.1 日志中间件装饰器

typescript 复制代码
function Log() {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const isAsync = descriptor.value.constructor.name === "AsyncFunction";
    
    if (isAsync) {
      const original = descriptor.value;
      descriptor.value = async function (...args: any[]) {
        const start = Date.now();
        console.log(`[START] ${target.constructor.name}.${propertyKey}`);
        
        try {
          const result = await original.apply(this, args);
          console.log(`[END] ${propertyKey} took ${Date.now() - start}ms`);
          return result;
        } catch (error) {
          console.log(`[ERROR] ${propertyKey}:`, error);
          throw error;
        }
      };
    } else {
      const original = descriptor.value;
      descriptor.value = function (...args: any[]) {
        console.log(`Calling ${propertyKey}`, args);
        return original.apply(this, args);
      };
    }
    
    return descriptor;
  };
}

// 使用
class OrderService {
  @Log()
  async createOrder(order: any) {
    await new Promise(r => setTimeout(r, 100));
    return { id: 1, ...order };
  }
  
  @Log()
  calculate(itemCount: number, price: number) {
    return itemCount * price;
  }
}

5.2 权限校验装饰器

typescript 复制代码
interface Role {
  name: string;
  level: number;
}

const roleLevels: Record<string, number> = {
  guest: 0,
  user: 1,
  admin: 2,
  superadmin: 3
};

function Require(levelOrRole: number | string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const requiredLevel = typeof levelOrRole === "number" 
      ? levelOrRole 
      : roleLevels[levelOrRole];
    
    const original = descriptor.value;
    
    descriptor.value = function (...args: any[]) {
      const currentUser = (globalThis as any).currentUser;
      
      // 假设从上下文获取用户级别
      const userLevel = currentUser?.role?.level ?? 0;
      
      if (userLevel < requiredLevel) {
        throw new Error(`需要权限级别 ${requiredLevel},当前 ${userLevel}`);
      }
      
      return original.apply(this, args);
    };
    
    return descriptor;
  };
}

// 使用
class AdminPanel {
  @Require(2)
  deleteUser(id: number) {
    console.log(`Deleted user ${id}`);
  }
  
  @Require("admin")
  modifySettings(settings: any) {
    console.log("Modified settings");
  }
}

六、元数据反射

6.1 使用 reflect-metadata

typescript 复制代码
import "reflect-metadata";

const METADATA_KEY = "design:paramtypes";

function Controller(path: string) {
  return function (target: new () => any) {
    // 在类上存储路由路径
    Reflect.defineMetadata("route:path", path, target);
    
    // 可以存储更多元数据
    Reflect.defineMetadata("route:middleware", [], target);
    
    return target;
  };
}

function Get(path: string) {
  return function (target: any, propertyKey: string) {
    // 存储方法路径
    Reflect.defineMetadata("route:handler", { path, method: "GET" }, target, propertyKey);
    
    // 存储参数类型
    const paramTypes = Reflect.getMetadata(METADATA_KEY, target, propertyKey);
    Reflect.defineMetadata("route:paramTypes", paramTypes, target, propertyKey);
  };
}

// 读取元数据
function getRouteMetadata(target: new () => any) {
  return {
    path: Reflect.getMetadata("route:path", target),
    handlers: Object.getOwnPropertyNames(target.prototype)
      .filter(k => k !== "constructor")
      .map(k => ({
        method: k,
        config: Reflect.getMetadata("route:handler", target.prototype, k)
      }))
  };
}

七、总结与实践建议

装饰器类型 ���用���景 经典案例
类装饰器 依赖注入、单例、AOP NestJS、Angular
方法装饰器 缓存、重试、事务 数据访问层
参数装饰器 参数校验、转换 API 入参处理
属性装饰器 依赖注入、自动装配 IoC 容器

最佳实践

  1. 不要过度装饰:每个装饰器都有开销,保持合理的抽象层级
  2. 类型安全:充分利用TypeScript的类型系统
  3. 调试友好:装饰器会增加调用栈,添加有意义的日志
  4. 组合优于继承:用装饰器组合代替类继承

互动讨论

你在项目中使用装饰器了吗?遇到过什么问题?

相关推荐
数据掘金9 分钟前
小程序埋点方案的埋点文档怎么自动生成?
前端
deli0079 分钟前
cron 表达式为什么总跑错?10 个场景实测,下次执行时间一眼看懂
前端
代码猎人13 分钟前
前端怎么优化页面性能
前端
代码猎人15 分钟前
前端怎么配合后端完成数据对接
前端
lexiangqicheng21 分钟前
Docker的沙箱机制是怎样的?
前端·javascript
燕落南枝1 小时前
HTML 表格标签与 AI 合并单元格实战指南
前端·人工智能·html
zyseo81 小时前
谷歌SEO 内链地图脚本:Python 画出权重分发,揪出孤岛页
java·前端·python
ClickHouseDB2 小时前
ClickHouse .NET 驱动程序新特性:从 1.0 到 1.3 的演进之路
java·前端·windows
bitbrowser2 小时前
Gmail注册要求手机号如何解决?哪些情况可以不填手机号
前端·指纹浏览器·gmail