Last updated on

Nest 系列 - 2:装饰器和类型解析

上一节,我们留了个问题:

但我们知道 类型源于 TS,而真正运行的时候被转换成 JS 是不带有类型的,那框架层「factory」又是如何做类型匹配的呢?

在探讨这个问题之前先来看看什么是装饰器

装饰器

建议结合官网一起看 https://www.typescriptlang.org/docs/handbook/decorators.html

装饰器首先是一个函数,其次他的用法不同于一般的函数调用而是用

function xxx() {}

@xxx
class A {}

其次这个函数的使用场景有限,仅在用类的场景,并且操作对象是下面其中的内容

  • Class
  • Method
  • Accessor
  • Property
  • Parameter

若几个场景同时存在,执行顺序如下

There is a well defined order to how decorators applied to various declarations inside of a class are applied:

  1. Parameter Decorators, followed by MethodAccessor, or Property Decorators are applied for each instance member.
  2. Parameter Decorators, followed by MethodAccessor, or Property Decorators are applied for each static member.
  3. Parameter Decorators are applied for the constructor.
  4. Class Decorators are applied for the class.
@classDecorator
class Point {
  [x: string]: any

  @functionDecorator()
  print() {}

  @functionDecorator()
  static staticPrint() {}

  // 因为我们的 functionDecorator2 ,本身便是装饰器
  @functionDecorator2
  print2() {}
}

function classDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
  console.log("run classDecorator", constructor)
  return class extends constructor {
    newProperty = "new property"
  }
}

// 生成装饰器的工厂函数
function functionDecorator() {
  console.log("run functionDecorator")
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // target 实例方法是实例, 静态方法是类的构造函数
    console.log("run functionDecorator", target, propertyKey, descriptor)
  }
}

function functionDecorator2(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // target 实例方法是实例, 静态方法是类的构造函数
  console.log("run functionDecorator2", target, propertyKey, descriptor)
}
run functionDecorator
run functionDecorator {} print {
  value: [Function: print],
  writable: true,
  enumerable: false,
  configurable: true
}
run functionDecorator2 {} print2 {
  value: [Function: print2],
  writable: true,
  enumerable: false,
  configurable: true
}
run functionDecorator
run functionDecorator [class Point] staticPrint {
  value: [Function: staticPrint],
  writable: true,
  enumerable: false,
  configurable: true
}
run classDecorator [class Point]

⚠️注意:这些行为是在 声明 class 的时候就进行的

Metadata

https://www.typescriptlang.org/docs/handbook/decorators.html#metadata

TS在编译过程中会去掉原始数据类型相关的信息,将TS文件转换为传统的JS文件以供JS引擎执行。但是,一旦我们引入reflect-metadata并使用装饰器语法对一个类或其上的方法、属性、访问器或方法参数进行了装饰,那么TS在编译后就会自动为我们所装饰的对象增加一些类型相关的元数据

  • 类型元数据使用元数据键”design:type”
  • 参数类型元数据使用元数据键”design:paramtypes”
  • 返回值类型元数据使用元数据键”design:returntype”
import "reflect-metadata"

function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(Reflect.getMetadata("design:type", target, propertyKey))
  console.log(Reflect.getMetadata("design:paramtypes", target, propertyKey))
  console.log(Reflect.getMetadata("design:returntype", target, propertyKey))
}

class Point {
  constructor() {}

  @validate
  setX(x: number, y: string): number {
    return "1" as any
  }
}

const p = new Point()
[Function: Function]
[ [Function: Number], [Function: String] ]
[Function: Number]

可以看到,我们在运行时,拿到了TS环境中的类型,这样就解决了文章开头所提的如何在运行时拿到类型的问题

如果我们想自己设置一些值在 matadata 中可以

// 还未成为标准,因此想使用reflect-metadata中的方法就需要手动引入该库,引入后相关方法会自动挂在Reflect全局对象上
import 'reflect-metadata'

class Example {
  text: string
}
// 定义一个exp接收Example实例,: Example/: string提供给TS编译器进行静态类型检查,不过这些类型信息会在编译后消失
const exp: Example = new Example()

// 注意:手动添加元数据仅为展示reflect-metadata的使用方式,实际上大部分情况下应该由编译器在编译时自动添加相关代码
// 为了在运行时也能获取exp的类型,我们手动调用defineMetadata方法为exp添加了一个key为type,value为Example的元数据
Reflect.defineMetadata('type', 'Example', exp)
// 为了在运行时也能获取text属性的类型,我们手动调用defineMetadata方法为exp的属性text添加了一个key为type,value为Example的元数据
Reflect.defineMetadata('type', 'String', exp, 'text')

// 运行时调用getMetadata方法,传入希望获取的元数据key以及目标就可以得到相关信息(这里得到了exp以及text的类型信息)
// 输出'Example' 'String'
console.log(Reflect.getMetadata('type', exp))
console.log(Reflect.getMetadata('type', exp, 'text'))

编译结果探究

利用 tsc 编译上面的文件

"use strict"
var __decorate =
  (this && this.__decorate) ||
  function (decorators, target, key, desc) {
    var c = arguments.length,
      r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
      d
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
      r = Reflect.decorate(decorators, target, key, desc)
    else
      for (var i = decorators.length - 1; i >= 0; i--)
        if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r
    return c > 3 && r && Object.defineProperty(target, key, r), r
  }
exports.__esModule = true
require("reflect-metadata")
function validate(target, propertyKey, descriptor) {
  console.log(Reflect.getMetadata("design:type", target, propertyKey))
  console.log(Reflect.getMetadata("design:paramtypes", target, propertyKey))
  console.log(Reflect.getMetadata("design:returntype", target, propertyKey))
}
var Point = /** @class */ (function () {
  function Point() {}
  Point.prototype.setX = function (x, y) {
    return "1"
  }
  __decorate([validate], Point.prototype, "setX")
  return Point
})()
var p = new Point()

看到这里 我们可以理解官网中

As such, the following steps are performed when evaluating multiple decorators on a single declaration in TypeScript:

  1. The expressions for each decorator are evaluated top-to-bottom.
  2. The results are then called as functions from bottom-to-top.

的含义了 1的部分就是 when composing functions f and g, the resulting composite (f ∘ g)(x) is equivalent to f(g(x)). 2的部分就是

for (var i = decorators.length - 1; i >= 0; i--)

  __decorate([validate], Point.prototype, "setX")

这一块更详细的部分可以参考 Nest.js入门 —— TS装饰器与元数据(二) 的第四节

🔗参考链接

Nest.js入门 —— TS装饰器与元数据(二) Decorators