- はじめに
- Decorator とは
- 実行してみる
- 改めて Decorator 関数が何を行なっているかを理解する
- Decorator Factory
- Decorator の種類
- Class Decorators
- Method Decorators
- Accessor Decorators
- Property Decorators
- Parameter Decorators
- おわりに
はじめに
この記事では Typescript の Decorator の挙動について基礎的な部分を解説します
動作の検証は以下のバージョンのツールで行ないました
$ tsc --version
Version 4.3.2
$ ts-node --version
v10.0.0
Decorator とは
Typescript の Decorator とは Class 内のメソッドやプロパティを修正したり変更したりできる関数の事です
公式にはここに説明があります
公式の説明によると、(2021年6月現在) Decorator は JavaScript の stage 2 proposal との事で、今後仕様に変更が入る可能性があります。またそのため、以下のように tsconfig で experimentalDecorators
flag を true にしないと使えません
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
実行してみる
Decorator は概念だけだとかなり解りづらいと思うので実行してみるのが理解の近道だとおもいます
簡単なコードを走らせて挙動を確認してみます
class TestClass {
@logError
throwError(): void {
throw new Error()
}
}
function logError(target: any, key: string, desc: PropertyDescriptor): void {
console.log("target", target)
console.log("key", key)
console.log("desc", desc)
const method = desc.value
desc.value = function() {
try {
method()
} catch (e) {
console.log("error catched")
}
}
}
new TestClass().throwError()
上記コードを適当な名前で保存して (ここでは test.ts) tsc-node を使って実行してみます
$ ts-node test.ts
target { throwError: [Function (anonymous)] }
key throwError
desc {
value: [Function (anonymous)],
writable: true,
enumerable: true,
configurable: true
}
error catched
どうやら TestClass の throwError メソッドが実行されて、その際に Decorator 関数内部で try catch に引っかかりコンソールログが出力されているようです
以下のコマンドで transpile して詳しくみてみます
$ tsc test.ts
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;
};
var TestClass = /** @class */ (function () {
function TestClass() {
}
TestClass.prototype.throwError = function () {
throw new Error();
};
__decorate([
logError
], TestClass.prototype, "throwError");
return TestClass;
}());
function logError(target, key, desc) {
console.log("target", target);
console.log("key", key);
console.log("desc", desc);
var method = desc.value;
desc.value = function () {
try {
method();
}
catch (e) {
console.log("error catched");
}
};
}
new TestClass().throwError();
Decorator 関数の第一引数である target には TestClass class の prototype が入り、第二引数にはデコレートした関数名が入り呼び出されています。またコードを見ると Decorator はクラスが定義された瞬間に実行されています。Decorator はクラスインスタンスが実体化されるタイミングや、デコレートされたメソッドなどが実行されるタイミングで呼ばれるわけでは無い事がわかります
Decorator 関数の第三引数に入る desc は __decorate 関数の getOwnPropertyDescriptor で取得された PropertyDescriptor が入ります
desc = Object.getOwnPropertyDescriptor(target, key)
PropertyDescriptor は ES5 で定義された JS object の property を記述する構造体です
console log された結果を見ると以下のようなものになっており、value, writable, enumerable, configurable という 4 つの property を持つ事がわかります
desc {
value: [Function (anonymous)],
writable: true,
enumerable: true,
configurable: true
}
ここで、 value
には decorate した関数そのものが入っています
getOwnPropertyDescriptor の挙動に関してはこちらで確認できます
改めて Decorator 関数が何を行なっているかを理解する
上記を踏まえて再度 Decorator 関数を見てみると、何を行なっているかが一目瞭然です
function logError(target: any, key: string, desc: PropertyDescriptor): void {
console.log("target", target)
console.log("key", key)
console.log("desc", desc)
const method = desc.value
desc.value = function() {
try {
method()
} catch (e) {
console.log("error ctched")
}
}
}
実際に行なっている事は非常にシンプルで主に以下の 2 つです
- この関数はクラスの定義コードが走った際に 1 度だけ実行され
- 実行時に PropertyDescriptor を受け取り、関数を try catch で wrap した関数を作り、それを property にセットし直す
これだけで元々の関数を変更せずに Decorator 関数がセットされた関数が呼び出された際に exception が発生すると catch して error を console に出してくれるようにできます
Decorator Factory
上の例では logError Decorator が catch した際に console.log するメッセージは定数でした
例えばメソッドによってメッセージを変更する、などを行いたい場合には Decorator Factory を使う事で可能です
function color(value: string) {
// this is the decorator factory, it sets up
// the returned decorator function
return function (target) {
// this is the decorator
// do something with 'target' and 'value'...
};
}
例えば logError の例だと、以下のように wrap する事で Decorator 関数に設定値を受け取る事ができます (ここで受け取る値はクラスの定義実行時に 1 度だけで、関数の実行時に値を受け取るわけでは無い事に注意する必要があります)
function logError(message: string) {
return function(target: any, key: string, desc: PropertyDescriptor): void {
console.log("target", target)
console.log("key", key)
console.log("desc", desc)
const method = desc.value
desc.value = function() {
try {
method()
} catch (e) {
console.log(message)
}
}
}
}
これを使う場合には以下のようになります
class TestClass {
@logError("throwError でエラーになったよ")
throwError(): void {
throw new Error()
}
@logError("process でエラーになったよ. やばい!!")
process(): void {
console.log('普通の処理')
}
}
Decorator の種類
ここまで Method Decorators をベースに挙動を説明してきましたが、Decorator には色々な種類があるので公式のコードを引用しつつ紹介します
Class Decorators
Class Decorators はクラスのコンストラクタに適用され、クラス定義への修正などを行います
Class Decorator は引数として constructor を受け取ります
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:
console.log(bug.reportingURL);
Class Decorator で property を追加したとしても TypeScript の type system には認識してくれないので、 ts-node
などで直接実行しようとするとエラーになるので注意が必要です
Method Decorators
Method Decorators は挙動の概要で説明したものです
引数として target
key
descriptor
を受け取ります
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
descriptor の property を使ってメソッドの挙動を変更する事が可能です
Accessor Decorators
Method Decorator と同じ挙動をします
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
@configurable(false)
get y() {
return this._y;
}
}
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
Property Decorators
property decorator は property に対して宣言します
Decorator 関数が受け取る引数は以下の 2 つになります
- class の prototype (もしくは class の constructor)
- member の名前
以下は
class Greeter {
@logPropsCalled
greeting: string;
constructor(message: string) {
this.greeting = message;
}
}
function logPropsCalled(target: any, propertyKey: string): void {
console.log(`${propertyKey} is called`)
}
console.log(new Greeter('message').greeting)
これを実行すると以下のようになります
$ ts-node greeter.ts
greeting is called
message
Parameter Decorators
パラメーター宣言の直前で宣言します
下記公式の例では、Method Decorator と組み合わせて、引数のチェックを行なっています
具体的には required Decorator で必須となる parameter を宣言し、validate で必須となる引数が undefined じゃない事をチェックしています
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
@validate
print(@required verbose: boolean) {
if (verbose) {
return `type: ${this.type}\ntitle: ${this.title}`;
} else {
return this.title;
}
}
}
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata( requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
let method = descriptor.value!;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
};
}
おわりに
Typescript の Decorator の基本について解説しました
Decorator は Experimental 機能でありながら数多くの Framework などでも採用されており、しっかりと理解しておいて損の無い機能だとおもいます