myfreax

TypeScript中的装饰器和metadata reflection API反射:从新手到专家

深入理解TypeScript的修饰器的实现,它让JavaScript实现反射和依赖注入成为可能。在这篇文章中我们将会学习,metadata反射API,基础类型序列化,复杂类型的序列化。

6 min read
By myfreax
TypeScript中的装饰器和metadata reflection API反射:从新手到专家

深入理解TypeScript的修饰器的实现,它让JavaScript实现反射和依赖注入成为可能。

教程分为四大部分

  • 第一部分:方法修饰器
  • 第一部分:属性修饰器&class修饰器
  • 第三部分:参数修饰器&修饰工厂
  • 第四部分:类型序列化&metadata反射API

在这篇文章中我们将会学习

  • 我们的JavaScript为什么需要反射
  • metadata反射API
  • 基础类型序列化
  • 复杂类型的序列化

为什么需要反射在JavaScript中?

反射常用于描述代码或者审核其它代码在同一个系统中

反射在组合,依赖注入,运行时类型断言,测试中非常有用

随着我们javascript应用越来越大,我们开始需要一些工具(像依赖反转控制,运行时类型断言)来管理应用增长的复杂性。现在的问题是JavaScript没有反射,这些工具或者特性将不能被实现,但是一些强大的编程语言实现的反射像 C#或者Java。

一个强大的反射API将允许我们测试未知的对象在运行的时候,找到所有关于它的信息。我们希望可以找到如下信息

  • 实体的名称
  • 实体的类型
  • 那个接口被实体实现
  • 实体的属性名称和类型
  • 实体的构造参数名称和类型

在JavaScript我们可以使用那个函数 Object.getOwnPropertyDescriptor() or Object.keys() 查找一些关于实体的信息,但我们需要反射实现更加强大的工具。

但是,这些情况将发生改变,因为TypeScript开始支持一些反射特性,让我们看看这些特性。

Metadata反射API

TypeScript团队开发人员使用Polyfilli垫片来为ES7添加反射API。TypeScript编译器现在可以为装饰器发出一些序列化的设计时元数据类型。

我们可以使用metadata反射API通过使用 reflect-metadata 包。

npm install reflect-metadata;

我们必须使用TypeScript1.5之后的版本和编译器标识emitDecoratorMetadata设置为true,我们也需要包含reflect-metadata.d.ts文件并载入Reflect.js。

我们接下来实现自己的修饰器并使用reflect metadata design key,但现在有三种类型的design key可用

  • 类型的metadatau使用metadata key"design:type"。
  • 参数的类型metadata使用metadata key "design:paramtypes"
  • 返回类型metadata使用metadata key "design:returntype"

让我们看几个示例。

获取属性的类型metadata使用reflect metadata API

让我们声明一个属性修饰器:

    function logType(target : any, key : string) {
      var t = Reflect.getMetadata("design:type", target, key);
      console.log(`${key} type: ${t.name}`);
    }

我们可以将它应用到class的一个属性中:


    class Demo{ 
      @logType // apply property decorator 应用属性修饰器
      public attr1 : string;
    }

上面的中console将会输出:

attr1 type: String

获取参数类型的metadata使用reflect metadata API

让我们声明一个参数修饰器:


    function logParamTypes(target : any, key : string) {
      var types = Reflect.getMetadata("design:paramtypes", target, key);
      var s = types.map(a => a.name).join();
      console.log(`${key} param types: ${s}`);
    }  

我们将它应用到class的一个方法上并获取参数类型信息。

    class Foo {}
    interface IFoo {}

    class Demo{ 
      @logParameters // apply parameter decorator 应用参数修饰器
      doSomething(
        param1 : string,
        param2 : number,
        param3 : Foo,
        param4 : { test : string },
        param5 : IFoo,
        param6 : Function,
        param7 : (a : number) => void,
      ) : number { 
          return 1
      }
    }

上面的示例中console将会输出:

doSomething param types: String, Number, Foo, Object, Object, Function, Function

获取返回类型的metadata使用reflect metadata API

我们也可以获取方法返回类型的信息使用"design:returntype" metadata key

Reflect.getMetadata("design:returntype", target, key);

基础类型的序列化

让我们再看看上面的design:paramtypes示例,注意接口IFoo和字面量对象{ test : string} 已序列化为对象,这是因为TypeScript只支持基础类型的序列化,下面是基础类型的序列化的规则。

  • 数值序列化为数值
  • string serialized as String
  • boolean serialized as Boolean
  • any serialized as Object
  • void serializes as undefined
  • Array serialized as Array
  • 如果是Tuple 序列化为Array
  • 如果是class 序列化为class的构造器
  • If an Enum serialized it as Number
  • 如果Enum 序列化Number
  • 如果它至少有一个调用签名,则序列化为Function
  • 否则序列化为对象Object ,包括接口interfaces

接口和字面量对象在未来也可以使用复杂的类型序列化,但是现在不可用。

复杂类型序列化

TypeScript团队正在研究一项提案,该提案将使我们能够生成复杂类型的元数据。

他们的建议描述了一些复杂类型将如何序列化。 上面的序列化规则仍将用于基本类型,但是复杂的类型将使用不同的序列化逻辑。 在提案中,有一个基本类型用于描述所有可能的类型:

/** 
  * Basic shape for a type.
  */
interface _Type {
  /** 
    * Describes the specific shape of the type.
    * @remarks 
    * One of: "typeparameter", "typereference", "interface", "tuple", "union", 
    * or "function".
    */
  kind: string; 
}

我们还可以找到用于描述每种可能类型的类。 例如,我们可以找到提议用于对遗传接口进行序列化的类foo <bar> {/ * ... * /}:

/**
  * Describes a generic interface.
  */
interface InterfaceType extends _Type {
  kind: string; // "interface"

  /**
    * Generic type parameters for the type. May be undefined.
    */
  typeParameters?: TypeParameter[];

  /**
    * Implemented interfaces.
    */
  implements?: Type[];

  /**
    * Members for the type. May be undefined. 
    * @remarks Contains property, accessor, and method declarations.
    */
  members?: { [key: string | symbol | number]: Type; };

  /**
    * Call signatures for the type. May be undefined.
    */
  call?: Signature[];

  /**
    * Construct signatures for the type. May be undefined.
    */
  construct?: Signature[];

  /**
    * Index signatures for the type. May be undefined.
    */
  index?: Signature[];
}

正如我们在上面看到的,将有一个指示已实现的接口的属性:

/**
* Implemented interfaces.
*/
implements?: Type[];

该信息可用于执行某些操作,例如验证实体是否在运行时实现了某些接口,这对于IoC容器可能真的有用。

我们不知道何时将复杂类型序列化支持添加到TypeScript,但我们迫不及待,因为我们计划使用它来为我们的JavaScript极佳IoC容器InversifyJS添加一些很酷的功能。

结论

我们已经知道如何使用metadata reflection API。