Skip to content
Hongzzz's Blog
Go back

Typescript 5 装饰器

Updated:

装饰器现在其实已经广泛应用于前端领域,以及一些框架和开发工具中。例如:在 React 开发中,Redux 使用装饰器来连接组件和状态;在 NestJS 框架中,装饰器被用于实现依赖注入和中间件。这些都是利用装饰器实现的,使我们的代码更加简洁,逻辑更加清晰。

随着 TypeScript 5.0 版本正式支持 Stage3 的 Decorators(装饰器) - 提案,装饰器这个特性可以被我们更加广泛的应用。且进入 Stage3 阶段的提案往往也代表着不会有大的变更,本文也将基于这个版本的装饰器使用进行入门的探索。

装饰器的历史

装饰器的概念源于 Python 和其他使用类似结构的编程语言,主要用于修改或扩展类、方法和属性的行为。

最初的 JavaScript 装饰器提案于 2014 年提出,自那时以来,已经开发出多个版本的提案,目前的版本正在 ECMAScript 标准化过程的第 3 阶段。

过往:实验性装饰器

实际上,TypeScript 在 5.0 版本之前就已经支持了实验性的装饰器。但是,由于它是基于较早的提案来实现的,因此与最新的提案存在一些差异:

  1. 需要在 tsconfig 中开启 experimentalDecorators
  2. decorator 方法与 Stage3 中的参数有差异
  3. 元数据需要通过 reflect-metadata 来获取,如类型等

本文省略这部分内容,感兴趣的可以查看这个文档 https://www.typescriptlang.org/docs/handbook/decorators.html

现在:Stage3 装饰器

现在基于 Stage3 提案的 Decorators 在默认配置下就可以使用了,如果 tsconfig 中指定了 experimentalDecorators 需要移除,否则还是会 fallback 到之前的实验性装饰器上。

新的装饰器 Decorator 的方法接受两个参数,类型定义如下:

type Decorator<T> = (
  value: T, // 对应的字段值
  context: DecoratorContext,
) => void | T;

以上的装饰器类型又分为以下几种:

Class、Method、Getter、Setter、Field、Accessor

type ClassMemberDecoratorContext =
    | ClassMethodDecoratorContext
    | ClassGetterDecoratorContext
    | ClassSetterDecoratorContext
    | ClassFieldDecoratorContext
    | ClassAccessorDecoratorContext;

type DecoratorContext =
    | ClassDecoratorContext
    | ClassMemberDecoratorContext;

Class

类型定义

展开查看
interface ClassDecoratorContext<
    Class extends abstract new (...args: any) => any = abstract new (...args: any) => any,
> {
    readonly kind: "class";
    readonly name: string | undefined;
    addInitializer(initializer: (this: Class) => void): void;
    readonly metadata: DecoratorMetadata;
}

实例

收集初始化的实例:

class InstanceCollector {
  instances = new Set();
  install = <Class extends Constructor>(
    value: Class,
    context: ClassDecoratorContext,
  ) => {
    const _this = this;
    return class extends value {
      constructor(...args: any[]) {
        console.log(
          "class constructor",
          context.name,
          context.metadata,
          context.kind,
        );
        super(...args);
        _this.instances.add(this);
      }
    };
  };
}

const collector = new InstanceCollector();

@collector.install
class MyClass {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

const inst1 = new MyClass("1");
const inst2 = new MyClass("2");
const inst3 = new MyClass("3");

console.log("instances: ", collector.instances);

/** Console
instances:  Set(3) {
  MyClass { name: '1' },
  MyClass { name: '2' },
  MyClass { name: '3' }
}
**/

Method

实际应用

个人认为最实用的一个装饰器,目前想到一些比较实用的用法如下:

示例

下面这个示例是对函数调用打 log 和 try catch 处理:

function log(originalMethod: Function, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);
  function replacementMethod(this: any, ...args: any[]) {
    const className = String(this.constructor.name);
    console.log(`LOG: Entering method '${className}.${methodName}'.`);
    const result = originalMethod.call(this, ...args);
    console.log(`LOG: Exiting method '${className}.${methodName}'.`);
    return result;
  }
  return replacementMethod;
}

function catchErr(
  originalMethod: Function,
  context: ClassMethodDecoratorContext,
) {
  const methodName = String(context.name);
  return function (this: any, ...args: any[]) {
    const className = String(this.constructor.name);
    try {
      const result = originalMethod.call(this, ...args);
      return result;
    } catch (e) {
      console.error(`Catch A Error in method '${className}.${methodName}'.`, e);
      return null;
    }
  };
}

class BankService {
  @log
  callAPI() {
    console.log("call api");
  }

  // 组合装饰器
  @log
  @catchErr
  callAPIWithError() {
    throw new Error("error");
  }
}

const service = new BankService();
service.callAPI();
service.callAPIWithError();
console.log("----exit-----");

Field

示例

function fieldD(val: any, context: ClassFieldDecoratorContext) {
  return function (initVal: any) {
    console.log(initVal, context.name);
    return initVal + 1;
  };
}

class A {
  @fieldD
  name: string = "name";

  @fieldD
  age: number = 18;

  @fieldD
  static staticName: string = "staticName";
}

const a = new A();
console.log(a);

/** Console
staticName staticName
name name
18 age
A { name: 'name1', age: 19 }
**/

Accessor

类装饰器引入了一个新命令 accessor

class C {
  accessor x = 1;
}

它其实是一种语法糖,相当于声明属性 x 是私有属性 #x 的存取接口。上面的代码等同于下面的代码:

class C {
  #x = 1;

  get x() {
    return this.#x;
  }

  set x(val) {
    this.#x = val;
  }
}

示例

function readonly<This, Return = number>(
  target: ClassAccessorDecoratorTarget<This, Return>,
  context: ClassAccessorDecoratorContext<This, Return>,
) {
  const result: ClassAccessorDecoratorResult<This, Return> = {
    get(this: This) {
      return target.get.call(this);
    },
    set(value: Return) {
      throw new Error(
        `Cannot assign to read-only property '${String(context.name)}'.`,
      );
    },
    init(this: This, value: Return) {
      console.log("init value", value);
      return value;
    },
  };

  return result;
}

class C {
  @readonly
  accessor x: number = 1;
}

const c = new C();

console.log(c.x);
c.x = 10; // Error: Cannot assign to read-only property 'x'.
console.log(c.x);

Getter/Setter

示例

以下示例中 getter/setter 都将原始值 * 2,并打印出相关过程:

function doubleGetter(value: any, context: ClassGetterDecoratorContext) {
  return function (this: any) {
    const result = value.call(this) * 2;
    console.log(
      "[doubleGetter]",
      "originalValue:",
      value.call(this),
      "doubleValue:",
      result,
    );
    return result;
  };
}

function doubleSetter(value: any, context: ClassSetterDecoratorContext) {
  return function (this: any, val: number) {
    const doubleValue = val * 2;
    console.log(
      "[doubleSetter]",
      "originalValue:",
      val,
      "doubleValue:",
      doubleValue,
    );
    return value.call(this, doubleValue);
  };
}

class Counter {
  _num: number;
  constructor() {
    this._num = 0;
  }
  @doubleGetter
  get num() {
    return this._num;
  }

  @doubleSetter
  set num(val: number) {
    this._num = val;
  }
}

const counter = new Counter();

counter.num = 1;
console.log(counter.num);

/** Console
[doubleSetter] originalValue: 1 doubleValue: 2
[doubleGetter] originalValue: 2 doubleValue: 4
4
**/

以上的 demo 代码放在 github 上: https://github.com/hongzzz/typescript-decorators-demo

参考


Share this post on:

Previous Post
JavaScript 数字分隔符(Numeric Separators)
Next Post
我的 Mac 工具