0%

TypeScript 注解使用

注解有什么用

可以让程序自动完成一些事情,从代码功能设计上,更高效、更清晰的完成业务功能解耦,让业务代码更聚焦于业务开发。

包括但不限于以下场景都可用 TypeScript 注解实现:

  1. 依赖注入 DI
  2. 运行前后的 Hook
  3. 转换请求参数、返回值 Transform
  4. 参数验证 Validation
  5. 监听属性变动 Watch
  6. 扩展属性和方法 Extra
  7. 序列化与反序列化 Serialization
  8. IDE 检查工具,如过期检查,通常只有几个规范,IDE默认适配。
  9. 测试框架工具

开启注解 TypeScript

使用注解之前,先将 tsconfig.json 里面的 experimentalDecorators 设置为 true

在 TypeScript 里面,注解也是一种类型,由于已经在使用 TypeScript, 所以定义的时候,建议直接加上注解类型。

例如:

const Proxy:ClassDecorator = (target) => {
  console.log(target)
}

@Proxy
class Abc{
  ...
}

编译执行后,就可以看到这个 target 已经被打印出来了,即便没有执行 new 操作。

这里的 ClassDecorator 就是 TypeScript 用于定义类注解的类型,定义的时候,加上注解类型可以规避一些小问题。

注解类型

在 TypeScript 里面,只有在 类 Class 里面才能使用注解。注解主要作用于类的五个位置, 对应的内置类型如下:

  1. 类 : ClassDecorator
  2. 方法 : MethodDecorator
  3. 属性 : PropertyDecorator
  4. 参数 : ParameterDecorator
  5. get/set : MethodDecorator 与方法相同

共4种内置类型,定义如下:

type ClassDecorator  = <TFunction extends Function>
     (target: TFunction) => TFunction | void;

type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;


type PropertyDecorator = 
    (target: Object, propertyKey: string | symbol) => void;

type ParameterDecorator = (
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) => void;

先做个试验 :

function f(key: string): any {
  console.log("evaluate: ", key);
  return function () {
    console.log("call: ", key);
  };
}

@f("Class Decorator")
class C {
  @f("Static Property")
  static prop?: number;

  @f("Static Method")
  static method(@f("Static Method Parameter") foo) {}

  constructor(@f("Constructor Parameter") foo) {}

  @f("Instance Method")
  method(@f("Instance Method Parameter") foo) {}

  @f("Instance Property")
  prop?: number;
}

编译、执行后,执行可以看到以下结果

evaluate:  Instance Method
evaluate:  Instance Method Parameter
call:  Instance Method Parameter
call:  Instance Method
evaluate:  Instance Property
call:  Instance Property
evaluate:  Static Property
call:  Static Property
evaluate:  Static Method
evaluate:  Static Method Parameter
call:  Static Method Parameter
call:  Static Method
evaluate:  Class Decorator
evaluate:  Constructor Parameter
call:  Constructor Parameter
call:  Class Decorator

可以看到输出的顺序如下:

  1. 方法 Method 和方法内的参数 Method Parameter 最先输出。
  2. 然后是内部属性 和 静态属性 Property Static Property
  3. 再输入静态方法,和静态方法参数 Static Method
  4. Classconstructor 最后输出

注解使用

以上四种不同类型的注解,通过不同的组合方式,可以实现种特异功能。

写几个简单的,且没有实现功能,大侠们自行举一反三。

注入属性、覆盖方法

type Consturctor = { new (...args: any[]): any };

function DI<T extends Consturctor>(BaseClass: T) {
  return class extends BaseClass {
      // 注入一个 logger 属性
      logger = new Loger();
      // 覆盖 toString 方法
      toString() {
        return JSON.stringify(this);
      }
  };
}

@DI
class A{
  
}

console.log(new A()) // {logger:object}

接口路由

// 如果需要在注解里面加入参数,可以用一个带参数的方法返回注解方法。
const Crontoller = (path: string) => Consturctor => {
  console.log(path)
  return class extends Consturctor {
      app.use(path)
  };
}


@Crontoller('/api')
class B{
  
}

控制反转

// 以一个 GraphQl 的 Query 为例
export const Query: MethodDecorator = (target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
  const resolver = target.constructor.name;
  Schema.Query[propertyKey] = function () {
      logger.info(propertyKey+' is begin call')
      descriptor.value.apply(getDomain(resolver), arguments);
      logger.info(propertyKey+' is called')
  }
}

class C{

  @Query
  queryAbc(){

  }
}

高级使用

看以看出,目前TypeScript 里面提供的注解,只是一串的脚本,通过脚本的组合来实现功能。

但这样的方法是有很大局限的,尤其是固定执行顺序就大大局限了思考问题的自由度。

如果想像 Java 里面使用反射的方式写注解,而不是拼脚本的方式,目前需要引入一个 reflect-metadata 的库。

比如:

// 说明,这一行最好写在应用程序的最上面
import 'reflect-metadata';

function validate(
  target: Object,
  key: string,
  descriptor: PropertyDescriptor
) {
  const originalFn = descriptor.value;

  // 获取参数
  const designParamTypes = Reflect
    .getMetadata('design:paramtypes', target, key);

  descriptor.value = function (...args: any[]) {
    args.forEach((arg, index) => {

      const paramType = designParamTypes[index];

      const result = arg.constructor === paramType
        || arg instanceof paramType;

      if (!result) {
        throw new Error(
          `Failed for validating parameter: ${arg} of the index: ${index}`
        );
      }
    });

    return originalFn.call(this, ...args);
  }
}

class C {
  @validate
  sayRepeat(word: string, x: number) {
    return Array(x).fill(word).join('');
  }
}

const c = new C();
c.sayRepeat('hello', 2); // OK
c.sayRepeat('', 'lol' as any); // 抛异常

小结

注解是一个很有用的东西,由于我也是浅层次使用,理解不深入,具体实践时,除了要反复调试外,还要能项目整体的技术结构与业务功能有更深入的了解才能写出好的注解。