💅

あらためてデコレーターを学ぶぞ

2022/07/22に公開

はじめに

  • NestJS入門してみようとしたら、アットマークから始まる記述を知らなかったので調べたことを簡単にまとめた記事です(デコレーターのことよくわかってなかった)
  • デコレーターの仕様は、過去にいろいろと変わって現在に至っているようでしたが、ようやく最近 ECMAScript では Stage 3 になり、TypeScript では今も実験的なサポート experimentalDecorators を使えますが、バージョン 4.8 で TC39 の Decorators を実装することが検討されているようです!
  • この記事は、TypeScriptのドキュメントのサンプルコードを動かしてみたり、あらためてNestJSのコード読んだりしてみた感想を書いてみました。
  • 最近のデコレーターを追えてない、NestJSを動かしてみようとしたらデコレーターってなんぞや、となったかたの参考になれば幸いです。

ESデコレーターの近況

  • まず、ESデコレーターの近況について。少し前ですがステージ3に!

https://github.com/tc39/proposal-decorators

https://twitter.com/robpalmer2/status/1508518054644400128

  • そして、TypeScriptでは4.8で実装される予定のようです!

https://github.com/microsoft/TypeScript/issues/48885

https://github.com/microsoft/TypeScript/issues/49074

デコレーターとは

  • デコレータは、クラス宣言、メソッド、アクセサ、プロパティ、またはパラメータにアタッチできる特別な種類の宣言のことです。

    • ざっくりいうと、クラスやメソッドを装飾してくれる機能です。
  • @expression の形式で記述します。

  • NestJSのコード例で示すと、@Controller@Get@Param などのことです。

    // NestJSのコード例
    
    @Controller('profiles')
    export class ProfileController {
      constructor(private readonly profileService: ProfileService) {}
      @Get(':username')
      async getProfile(@User('id') userId: number, @Param('username') username: string): Promise<ProfileRO> {
        return await this.profileService.findProfile(userId, username);
      }
    }
    

デコレーターファクトリー

  • デコレーターは、クラスやメソッドを装飾してくれる機能ですが、その装飾してくれる処理内容を記述するのがデコレーターファクトリーです。
  • データファクトリーを使うと、デコレーターによって呼び出される式を返す関数をカスタマイズすることができます。

https://www.typescriptlang.org/docs/handbook/decorators.html#decorator-factories

  • サンプルコード

    // デコレーターファクトリー
    function first() {
      console.log("first(): factory evaluated");
      return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("first(): called");
      };
    }
    
    // デコレーターファクトリー
    function second() {
      console.log("second(): factory evaluated");
      return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("second(): called");
      };
    }
    
    class ExampleClass {
      @first() // デコレーター
      @second() // デコレーター
      method() {}
    }
    
    // コンソールへの出力
    //  first(): factory evaluated
    //  second(): factory evaluated
    //  second(): called
    //  first(): called
    

デコレーターの種類

デコレーターを宣言できる場所によっていくつか種類があります。

クラスデコレーター

  • クラスの宣言前に宣言するデコレーターです。

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

  • クラスデコレーターの式は、実行時に関数として呼び出され、デコレートされたクラスのコンストラクタが唯一の引数になります。

    // クラスデコレーターの式
    function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
      return class extends constructor {
        reportingURL = "http://www...";
      };
    }
    
    @reportableClassDecorator
    class BugReport {
      type = "report";
      title: string;
    
      constructor(t: string) {
        this.title = t;
      }
    }
    
    const bug = new BugReport("Needs dark mode");
    console.log(bug.title); // Prints "Needs dark mode"
    console.log(bug.type); // Prints "report"
    
    // Note that the decorator _does not_ change the TypeScript type
    // and so the new property `reportingURL` is not known
    // to the type system:
    // @ts-ignore
    console.log(bug.reportingURL); // Prints "http://www..."
    

メソッドデコレーター

  • メソッドの宣言前に宣言するデコレーターです。

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

  • メソッドデコレーターの式は、実行時に関数として呼び出されます。

  • 次の3つの引数を取ります。

    • target 静的メンバーのクラスのコンストラクター関数、またはインスタンスメンバーのクラスのプロトタイプのいずれか(Either the constructor function of the class for a static member, or the prototype of the class for an instance member.)
    • propertyKey メンバーの名前(The name of the member.)
    • descriptor メンバーのプロパティ記述子、object の property(The Property Descriptor for the member.)
  • サンプルコード

// デコレーターファクトリー
function change(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 値を出力してみる
    console.log('target =>', target);
    console.log('propertyKey =>', propertyKey);
    console.log('descriptor =>', descriptor);

    // 関数を書き換えてみる例
    const addFunc = descriptor.value;
    descriptor.value = function () {
      const result = addFunc.apply(this);
      if (value) {
        return 'こんにちは';
      }
      return result
    };
  };
}

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
 
  @change(true)
  greet() {
    return "Hello, " + this.greeting;
  }
}

// 値を出力してみる
console.log('new Greeter("World").greet() =>', new Greeter("World").greet());
  • コンソールの出力結果
// console.logの出力結果
target => { greet: [Function (anonymous)] } // 静的メンバーのクラスのコンストラクター関数、またはインスタンスメンバーのクラスのプロトタイプのいずれか
propertyKey => greet
descriptor => {
  value: [Function (anonymous)], // デコレーとした関数そのものが入ってる
  writable: true,
  enumerable: true,
  configurable: true
}
new Greeter("World").greet() => こんにちは

アクセサデコレーター

  • アクセサ宣言の直前に宣言されるデコレーターです。

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

プロパティーデコレーター

  • プロパティ宣言の直前に宣言されるデコレーターです。

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

パラメータデコレーター

  • パラメータ宣言の直前に宣言されるデコレーターです。

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

NestJSとデコレーター

  • NestJSは、デコレーターの言語機能を中心にして作られていて、デコレーターがたくさん使われています。デコレーターのドキュメントをひととおり眺めてみたので、あらためてNestJSのコードを簡単に見てみました。

https://docs.nestjs.com/custom-decorators

  • コントローラーの一例。AppControllerクラスの上にControllerデコレータが記述されています。デコレーターを使ってクラスを装飾することでコントローラーとして使えるようにしています。
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}
  • NestJSの@Controllerのデコレーターのコードのコードを探してみた

https://github.com/nestjs/nest/blob/master/packages/common/decorators/core/controller.decorator.ts#L152

  • NestJSの@Getのデコレーターのコードを探してみた

https://github.com/nestjs/nest/blob/df4d99d92e7039e10a5f33f11df222468401159d/packages/common/decorators/http/request-mapping.decorator.ts#L57

おわりに

  • デコレーターについて調べたことを簡単にまとめてみました。
  • NestJSに限らず、デコレーターが採用されているライブラリはたくさんあると思うので、ちゃんと使い方を理解したら、便利そうで良さそうな機能だと思いました。
  • 過去、色々仕様が変わったりなどで歴史的な経緯がありそうでしたが、最近ではステージ3になりTypeScript4.8にも入りそうなので、もう少しちゃんと使い方をチェックしておきたいと思いました。

https://qiita.com/printf_moriken/items/b081f50b30d73275c801

参考

デコレーターを調べてて読んだリンク集です!勉強になりました!ありがとうございました!

https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841

https://zenn.dev/miruoon_892/articles/365675fa5343ed

https://www.webprofessional.jp/javascript-decorators-what-they-are/

https://qiita.com/taqm/items/4bfd26dfa1f9610128bc

https://qiita.com/tak001/items/ab573b8a5ba7b78b341a

https://qiita.com/Quramy/items/e3a43bb1734b8a7331e8

https://hachibeechan.hateblo.jp/entry/2021/01/29/NestJSちょびっとだけ触ってみた所感

https://cybozu.github.io/frontend-expert/posts/tc39-meeting-2022-03

https://info.drobe.co.jp/blog/engineering/typescript-decorator

https://zenn.dev/okunokentaro/articles/01ejkz0015emczdgwwe03nhace

GitHubで編集を提案
株式会社モニクル

Discussion