Typescript v5 Decorator 學習筆記

參考文章

Introduction

TS v5 版本的 decorator 在寫法上有更新,以下是相關介紹筆記

主要分為以下幾種:

type Decorator =
  | ClassDecorator
  | ClassMethodDecorator
  | ClassGetterDecorator
  | ClassSetterDecorator
  | ClassFieldDecorator
  | ClassAutoAccessorDecorator;

1. Class decorators

類型

type ClassDecorator = (
  value: Function,
  context: {
    kind: 'class';
    name: string | undefined;
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

範例

class InstanceCollector {
  instances = new Set();
  install = (value, {kind}) => {
    if (kind === 'class') {
      const _this = this;
      return function (...args) { // (A)
        const inst = new value(...args); // (B)
        _this.instances.add(inst);
        return inst;
      };
    }
  };
}

const collector = new InstanceCollector();

@collector.install
class MyClass {}

const inst1 = new MyClass();
const inst2 = new MyClass();
const inst3 = new MyClass();

console.log(collector.instances);

2. Class method decorators

類型

type ClassMethodDecorator = (
  value: Function,
  context: {
    kind: 'method';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown };
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

範例

function deprecatedMethod(target: Function, context: ClassMethodDecoratorContext) {
  if (context.kind === 'method') {
    const methodName = String(context.name);
    return function (...args: any[]) {
      console.log(`${methodName} is deprecated and will be removed in a future version.`)
      return target.apply(this, args);
    }
  }
}

class Person {
  @deprecatedMethod
  greet() {
    console.log('Hello, my name is');
  }
}

3. Class getter decorators, class setter decorators

類型

type ClassGetterDecorator = (
  value: Function,
  context: {
    kind: 'getter';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown };
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

type ClassSetterDecorator = (
  value: Function,
  context: {
    kind: 'setter';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { set: (value: unknown) => void };
    addInitializer(initializer: () => void): void;
  }
) => Function | void;

範例

function lazy(getter: Function, { kind, name }: ClassGetterDecoratorContext) {
  if (kind === 'getter') {
    return function () {
      const result = getter.call(this);
      Object.defineProperty(this, name, {
        value: result,
        writable: false,
      });
      return result;
    };
  }
}

class C {
  @lazy
  get value() {
    console.log('COMPUTING');
    return 'Result of computation';
  }
}

const inst = new C();
console.log('1 inst.value', inst.value);
console.log('2 inst.value', inst.value);
console.log('3 end');

// COMPUTING
// 1 inst.value Result of computation
// 2 inst.value Result of computation
// 3 end

4. Class field decorators

類型

type ClassFieldDecorator = (
  value: undefined,
  context: {
    kind: 'field';
    name: string | symbol;
    static: boolean;
    private: boolean;
    access: { get: () => unknown, set: (value: unknown) => void };
    addInitializer(initializer: () => void): void;
  }
) => (initialValue: unknown) => unknown | void;

範例

function twice(_: undefined, { kind }: ClassFieldDecoratorContext) {
  if (kind === 'field') {
    return (initialValue) => initialValue * 2;
  }
}

class C {
  @twice
  double = 3;
}

const inst = new C();
console.log(inst.double);
// 6

5. Auto-accessors: a new member of class definitions

在 class 欄位前加上 accessor 即可宣告,與普通欄位的差別如下:

  • A field creates either:
    • Properties (static or instance)
    • Private slots (static or instance)
  • An auto-accessor creates a private slot (static or instance) for the data and
    • A public getter-setter pair (static or prototype)
    • A private getter-setter pair (static or instance)
      • Private slots are not inherited and therefore never located in prototypes.

假設如下範例:

class C {
  accessor str = 'abc';
}
const inst = new C();
console.log(inst.str); // abc
inst.str = 'def';
console.log(inst.str); // def

等同於下方

class C {
  #str = 'abc';
  get str() {
    return this.#str;
  }
  set str(value) {
    this.#str = value;
  }
}

使用範例

const UNINITIALIZED = Symbol('UNINITIALIZED');

function readOnly({get,set}, {name, kind}) {
  if (kind === 'accessor') {
    return {
      init() {
        return UNINITIALIZED;
      },
      get() {
        const value = get.call(this);
        if (value === UNINITIALIZED) {
          throw new TypeError(
            `Accessor ${name} hasn’t been initialized yet`
          );
        }
        return value;
      },
      set(newValue) {
        const oldValue = get.call(this);
        if (oldValue !== UNINITIALIZED) {
          throw new TypeError(
            `Accessor ${name} can only be set once`
          );
        }
        set.call(this, newValue);
      },
    };
  }
}

class Color {
  @readOnly
  accessor name;
  constructor(name) {
    this.name = name;
  }
}

可以明顯看出與前面 field @readonly decorator的差別,這邊不需要在 class 外包裹一層

Last Updated:
Contributors: johnnywang