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. 组合优于继承:用装饰器组合代替类继承

互动讨论

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

相关推荐
AI砖家1 小时前
Vue3组件传参大全,各种传参方式的对比
前端·javascript·vue.js
希望永不加班1 小时前
var局部变量类型推断的利弊
java·服务器·前端·javascript·html
threelab2 小时前
Three.js 3D 地图可视化 | 三维可视化 / AI 提示词
前端·javascript·人工智能·3d·着色器
爱怪笑的小杰杰2 小时前
Leaflet 高性能大数据量图圆:彻底解决缩放/拖拽偏移问题
大数据·前端·vue.js·贴图
WL_Aurora2 小时前
大数据技术之SparkCore
大数据·前端·spark·rdd
失眠的咕噜3 小时前
PDA 安卓设备上传多张图片
android·前端·javascript
掰头战士3 小时前
深入了解JS原型及原型继承链机制
javascript
贵州数擎科技有限公司3 小时前
霓虹沙尘暴的 Three.js 实现
前端·webgl