Skip to content

裝飾器 Decorators

裝飾器目前還是 JS 的實驗性語法,要在 TS 使用它之前需要在 tsconfig.json 裡將 experimentalDecorators 選項給打開:

ts
{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

Decorators

TypeScript 的裝飾器為一個函式,並以 @expression 的形式去使用,expression 求值後必須為一個函式,它會在執行時被呼叫。

Class Decorator

先來寫一個最基本的類別裝飾器:

ts
function Logger(constructor: Function) {
  console.log('Logging...');
  console.log(constructor);
}

@Logger
class Person {
  name = 'Sheep';

  constructor() {
    console.log('Creating person object...');
  }
}

const pers = new Person();

console.log(pers);

此時在控制台打印出來的東西長這樣:

我們可以發現 Decorator 會先被打印出來,然後才是 constructor 中的 log,這是因為 Decorator 是在定義類別時執行的,而不是在類別被實例化時執行。

Decorator Factory

除了上面那種寫法之外,還可以定義一個裝飾器工廠 (Decorator Factory),它基本上會回傳一個裝飾器函式,但是我們可以將它做為裝飾器指派給某個需要裝飾的東西。

ts
// 使用裝飾器工廠我們就能接受傳參,並讓內部回傳的裝飾器函式可以使用
function Logger(logString: string) {
  return function (constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

// 使用方式就是直接在這呼叫並傳入自訂的參數
@Logger('LOGGING - PERSON')
class Person {
  name = 'Sheep';

  constructor() {
    console.log('Creating person object...');
  }
}

此時可以看到打印出的結果出現了我們自訂的字串:

這可以讓我們更加靈活的去使用裝飾器。

我們還可以同時使用多個裝飾器:

ts
function Logger(logString: string) {
  console.log('LOGGER FACTORY');
  return function (constructor: Function) {
    console.log(logString);
    console.log(constructor);
  };
}

function WithTemplate(template: string, hookId: string) {
  console.log('TEMPLATE FACTORY');
  return function (constructor: any) {
    console.log('Rendering template');
    const hookEl = document.getElementById(hookId);
    const p = new constructor();
    if (hookEl) {
      hookEl.innerHTML = template;
      hookEl.querySelector('h1')!.textContent = p.name;
    }
  };
}

@Logger('LOGGING')
@WithTemplate('<h1>My Person Object</h1>', 'app')
class Person {
  name = 'Sheep';

  constructor() {
    console.log('Creating person object...');
  }
}

裝飾器的執行順序為 bottom-up

Property Decorator

裝飾器還可以使用在類別中的欄位裡:

ts
function Log(target: any, propertyName: string | Symbol) {
  console.log('Property decorator!');
  console.log(target, propertyName);
}

class Product {
  @Log
  title: string;
  private _price: number;

  set price(val: number) {
    if (val > 0) {
      this._price = val;
    } else {
      throw new Error('Invalid price - should be positive!');
    }
  }

  constructor(t: string, p: number) {
    this.title = t;
    this._price = p;
  }

  getPriceWithTax(tax: number) {
    return this._price * (1 + tax);
  }
}