on
TypeScriptのDecoratorsについて調べた
Tags:
#TypeScriptはじめに
ライブラリ等でよくみるTypeScriptのDecoratorsについて調べてみました。有名なライブラリではTypeORMもDecoratorsを利用しています。(TypeORMはTypeScriptで書かれたORMapperライブラリ) Decoratorsは一見するとJavaのアノテーションのようなものです。 TypeORMのEntityの定義の例は以下です。
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
age: number;
}
クラスやフィールドに@~~がついています。 これがDecoratorsです。Decoratorsの仕様自体は現在(2019年9月時点)プロポーザルの段階(ステージ2)です。
TypeScriptではtsconfigの設定値を変更することによって利用できます。 TypeScript の Decorators についてのドキュメントは以下にあります。
詳しくは上記のドキュメントを参照してください。
今回はどのようにTypeScriptのDecoratorsを実装するのかを調べました。
tsconfig.json
公式ドキュメントにも書いてありますが、Decoratorsの機能を利用する場合はcompilerOptions
のtarget
をES5以上にし、experimentalDecorators
をtrue
にする必要があります。
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
Decoratorsの種類
Decoratorsには種類があります。指定された場所に@関数名
を書くことによって特定のDecoratorsとして見なされます。
Decoratorsの種類は以下の5つです。
- Class Decorators
- Method Decorators
- Property Decorators
- Accessor Decorators
- Parameter Decorators
Decoratorsの実態は関数です。
@関数名
でその関数が適用されます。
Decoratorsを定義する関数には自動的に引数に値が入ります。
Decoratorsの実装
以下のサンプルのコードを利用して、Decoratorsの実装について考えます。 サンプルで扱うDecoratorsはClass Decorators,Method Decorators,Property Decoratorsの3つです。
サンプルコードはGitHubにあります。
サンプルコード:
console.log("start of class definition\n");
@classDecorators
class Adding {
@propertyDecorators
baseNumber: number;
constructor(baseNumber: number) {
this.baseNumber = baseNumber;
}
@multiply(2)
add(plus: number) {
return (this.baseNumber += plus);
}
}
console.log("end of class definition\n");
function classDecorators(constructor: Function) {
console.log("class\n");
}
function propertyDecorators(target: Object, propertyKey: string) {
console.log("property\n");
}
function multiply(num: number) {
return (
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
) => {
console.log("method\n");
const addFunc = descriptor.value;
descriptor.value = function(...args: any) {
const result = addFunc.apply(this, args);
return result * num;
};
};
}
console.log("before initialize class\n");
const adding = new Adding(1);
console.log("after initialize class\n");
console.log(adding.add(1));
adding.baseNumber = 3;
console.log(adding.add(1));
上記の例では足し算を表すAdding
クラスに対してDecoratorsを適用しました。
Adding
クラスにはbaseNumberという足し算の元になる数(足される数)を表すプロパティとaddという足し算を表すメソッドがあります。
Class Decorators
Class Decoratorsはクラス宣言の上に@関数名
を記述します。
今回の例ではAddingクラス定義の上に@classDecoratorsと記述されているのがわかります。
この記述によって呼び出される関数がclassDecorators関数です。
classDecorators関数はは”class”と標準出力する単純なものです。
function classDecorators(constructor: Function) {
console.log("class\n");
}
class Decoratorsでは値を返すことができ、値を返した場合は、クラスのコンストラクタ関数がその値で置き換えられます。
Propterty Decorators
Property Decoratorsはクラスのプロパティの上に@関数名
を記述します。
今回はの例ではbaseNumberの上に@propertyDecoratorsと記述されているのがわかります。
この記述によって呼び出される関数がpropertyDecorators関数です。
propertyDecorators関数は”property”と標準出力する単純なものです。
function propertyDecorators(target: Object, propertyKey: string) {
console.log("property\n");
}
- 静的メンバであればクラスのコンストラクタ関数、インスタンスメンバであればクラスのprototype
- メンバの名前
Property Decoratorsでは値を返すことはできません。
Decorator Factory
Method Decoratorsの前にDecorator Factoryについて説明します。(サンプルコードのMethod DecoratorsでDecorator Factoryを利用しているため。) Decorator FactoryはDecoratorsをカスタマイズしたい場合に利用します。 Decorator Factoryを利用することによってDecoratorsに対するオプションの引数を受け取ることが可能になります。 Decorator FactoryはDecoratorsの関数を返します。
例
function color(value: string) { // this is the decorator factory
return function (target) { // this is the decorator
// do something with 'target' and 'value'...
}
}
Method Decorators
Property Decoratorsはクラスのメソッドの上に@関数名
を記述します。
今回の例ではaddメソッドの上に@multiplyと記述されています。
この記述によって呼び出されるのがmultiply関数です。
multiply関数は引数で渡ってきた数字をaddメソッドの結果にかける(かけ算する)という関数で、addメソッドを上書きしています。
今回のmultiply関数ではDecorator Factoryを利用しています。
function multiply(num: number) {
return (
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
) => {
console.log("method\n");
const addFunc = descriptor.value;
descriptor.value = function(...args: any[]) {
const result = addFunc.apply(this, args);
return result * num;
};
};
}
Method Decoratorsの関数の引数には以下の3つが渡されます。
- 静的メンバ(メソッド)であればクラスのコンストラクタ関数、インスタンスメンバ(メソッド)であればクラスのprototype
- メンバの名前(メソッド)
- メンバのプロパティディスクリプタ
もしかするとプロパティディスクリプタはあまり馴染みがないかもしれません。 もし、初めて聞いたという場合は以下のページのプロパティディスクリプタの説明を参照してみてください。
Object.getOwnPropertyDescriptor() - JavaScript | MDN
今回はプロパティディスクリプタのvalue属性を利用しています。 descriptor.valueにはメンバの値が入っています。 なので、今回の場合はdescriptor.valueでadd関数が取得できます。
const addFunc = descriptor.value;
descriptor.value = function(...args: any[]) {
const result = addFunc.apply(this, args);
return result * num;
};
applyについては以下を参照してください。
Function.prototype.apply() - JavaScript | MDN
今回は実行時のthisにクラスのオブジェクトをバインドしたいために、applyを利用しました。 thisをバインドしないと、add関数を実施した際に、this.baseNumberの部分でエラーが発生します。
function(...args: any[])
には実際にはnumber型の値が一つはいるだけです。
これもapply関数の第二引数が配列しか受け取らないためこうしています。(function(…args: any[])はよく見る書き方、applyと一緒に覚えると良さそうです。)
最後にresult * num
でaddFunc.apply(this, args)
no
実行結果に@multiply(num)で受け取ったnumをかけています。
サンプルコードを実行した結果
tsc
コマンドでコンパイルした後、node
で実行しました。
結界は以下のようになりました。
❯ node dist/index.js
start of class definition
property
method
class
end of class definition
before initialize class
after initialize class
---------------------------------
4
8
4 = (1 + 1) * 2
8 = (3 + 1) * 2
この結果からクラス定義の時点でDescriptorsが読み込まれていることがわかります。
おわりに
TypeScriptのDecoratorsについて調べてみました。 Decoratorsについて学ぶ時ドキュメントを見るだけでなく実際に使われているライブラリの実装をみてみるのも勉強になります。 いきなりTypeORMの実装を追うのはかなり大変なので、小さいライブラリから実装を見るのがお勧めです。 以下のtypescript-memoizeは小さくてお勧めです。
(メソッドをメモ化してくれるライブラリ) darrylhodgins/typescript-memoize
今後も継続してTypeScriptについて勉強していきたいと思います。