前回に引き続き、本記事では、TypeScriptのジェネリクスとデコレータについて説明します。
ジェネリクスとデコレータは、TypeScriptの高度なトピックですが、
TypeScriptを実践的に使いこなすためには必須の知識です。
【目次】
- TypeScript入門:型定義の基本と使い方
- TypeScript実践1:ジェネリクスとデコレータの使い方(←いまここ)
- TypeScript実践2:高度なジェネリクスとデコレータの使い方
- TypeScript実践3:インターセクション型とユニオン型
- TypeScript実践4:コンディショナル型
- TypeScript実践4:高度なデコレータの活用法
ジェネリクスとは?
ジェネリクスは、型をパラメータ化する仕組みです。
具体的には、あらゆる型に対応する汎用的な処理を実現することができます。
例えば、以下のようなidentity関数は、引数として渡された値をそのまま返すだけの関数です。
function identity(arg: any): any {
return arg;
}
identity('サンプル文字列') // 引数として渡された値をそのまま返す
この関数は、引数としてどのような値でも受け取ることができますが、戻り値の型が引数の型と同じであることを保証することができません。
これを、ジェネリクスを使って書き換えると以下のようになります。
function identity<T>(arg: T): T {
console.log(arg)
return arg;
}
identity<string>('サンプル文字列') // string型の返却値であることを保証できる
このように、<T>
という記法を使って、型パラメータを定義することができます。
この場合、Tは任意の型を表します。(型用の引数のようなイメージです。)
Tでなくても問題ありませんが、慣用的にTを使うことが多いです。
デコレータとは?
デコレータは、クラスやメソッド、プロパティなどに対して、実行時に何らかの処理を行うための仕組みです。TypeScriptでは、デコレータを使って、クラスにメタデータを付加したり、DIコンテナを構築したりすることができます。
※DIコンテナ とは、「Dependency Injection(依存性注入)」といわれるコードの再利用性や保守性、テストの容易性など向上させる設計パターンの一つです。
例えば、以下のようにlogというデコレータを定義したとします。
// targetに何が渡されるかは場合によって異なるので、anyになります。
// nameはプロパティの名前が来るため、string型になります。
// PropertyDescriptorはTypeScriptに組み込まれれている型です。
function log(target: any, name: string, descriptor: PropertyDescriptor) {
const sampleMethod = descriptor.value;
// ログを出力する
descriptor.value = function(...args: any[]) {
console.log(`[${name}] Start`);
const result = sampleMethod.apply(this, args);
console.log(`[${name}] End`);
return result;
};
return descriptor;
}
このデコレータは、メソッドが呼び出される前後にログを出力する機能を持っています。これを、以下のようにメソッドに対して適用することができます。
class SampleClass {
@log
public logOutputMethod() {
console.log("logOutputMethod is called");
}
}
const output = new SampleClass();
output.logOutputMethod()
下記のようなログが出力されます。
[LOG]: "[logOutputMethod] Start"
[LOG]: "logOutputMethod is called"
[LOG]: "[logOutputMethod] End"
このように、@
を使ってデコレータを指定し、その上に実行したいデコレータの関数を記述することで、利用することができます。
ジェネリクスとデコレータを組み合わせる
ジェネリクスとデコレータを組み合わせることで、より柔軟なコードを記述することができます。
例えば、以下のようにジェネリックなRepositoryクラスで試してみます。
class Repository<T> {
private data: T[] = [];
public add(item: T) {
this.data.push(item);
}
public getAll(): T[] {
return this.data;
}
}
このクラスは、任意の型のオブジェクトを保持することができます。
ここで、transaction
というデコレータを定義してみます。
function transaction<T>(repository: Repository<T>) {
return function(
target: any,
name: string,
descriptor: PropertyDescriptor
) {
const sampleMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`[Transaction] Start`);
const result = sampleMethod.apply(this, args);
console.log(`[Transaction] End`);
repository.add(result);
return result;
};
return descriptor;
};
}
このデコレータは、メソッドの実行前にトランザクションを開始し、実行後にトランザクションを終了し、結果をRepositoryに保存するという内容になります。これを、以下のようにクラスに対して適用することができます。
class SampleClass {
@transaction(new Repository<string>())
public logOutputMethod(): string {
console.log("logOutputMethod is called");
return "result";
}
}
const output = new SampleClass();
output.logOutputMethod()
出力結果
[LOG]: "[Transaction] Start"
[LOG]: "logOutputMethod is called"
[LOG]: "[Transaction] End"
以上のように、ジェネリックなRepositoryとデコレータtransactionを組み合わせることで、クラスのメソッドをトランザクションでラップし、その結果をRepositoryに保存するということができます。
まとめ
今回は、TypeScriptのジェネリクスとデコレータについて説明しました。
難しいなという場合は、まずは下記の内容を覚えておくだけでいいと思います。
- ジェネリクス
- 型をパラメータ化する仕組み
- 汎用的な処理を実現するために必須の知識
- デコレータ
- クラスやメソッド、プロパティなどに対して実行時に何らかの処理を行う仕組み
- DIコンテナやログ出力などの機能を実現するために必須の知識
ジェネリクスとデコレータを組み合わせることで、より柔軟なコードを記述することができるため、Typescriptを勉強するにあたっては、是非マスター出来るようにしていきたい内容かなとお思います。