TypeScript:DI コンテナ使用時に抽象クラス(abstract class)をインターフェースとして利用する
November 1, 2022
TypeScript でインターフェースを定義したい場合、interface キーワードあるいは type キーワードを使用することがほとんどだと思います。
// interface で定義
interface IUserRepository {
findById(id: string): Promise<UserEntity>;
}
// type で定義
type IUserRepository = {
findById(id: string): Promise<UserEntity>;
};
// 実装
class UserRepository implements IUserRepository {
findById(id: string): Promise<UserEntity> {}
}
他方、あまり見かけない書き方かもしれませんが、abstract class をインターフェース定義のように使用することも可能です。(abstract class には実装を持たせることができますが、ここではインターフェース定義としてだけ利用します。)
// abstract class で定義
abstract class IUserRepository {
abstract findById(id: string): Promise<UserEntity>;
}
// 実装
class UserRepository implements IUserRepository {
findById(id: string): Promise<UserEntity> {}
}
さて、abstract class でのインターフェース定義は、interface あるいは type での定義にはない特徴があります。
それはランタイム時でもクラスの定義が参照できるという点です。
TypeScript はコンパイルすると単なる JavaScript ファイルになります。そのため interface や type の定義はランタイム時には消えています。
一方で abstract class はクラスなので、ランタイム時にも残っており参照可能です。
これを有効活用できる場合があります。その一例が DI コンテナを使用するときです。
TSyringe の例 #
DI コンテナの例として TSyringe というライブラリを使用する想定の記述します。類似の Inversify というライブラリも有名です。
interface あるいは type を使用した失敗例 #
interface あるいは type はランタイム時には残らないため、以下はエラーになってしまいます。
import { InjectionToken, container, injectable } from 'tsyringe';
interface IUserRepository {
findById(id: string): Promise<UserEntity>;
}
class UserRepository implements IUserRepository {
findById(id: string): Promise<UserEntity> {}
}
// エラー:'IUserRepository' only refers to a type, but is being used as a value here.
// IUserRepository はランタイム時には消えるため、キーとして利用ができない。
container.register(IUserRepository as InjectionToken, UserRepository);
@injectable()
class FindUserUseCase {
constructor(private userRepository: IUserRepository) {}
run(id: string): Promise<UserEntity> {
return this.userRepository.findById(id);
}
}
interface あるいは type を使用した成功例 #
キーとして interface あるいは type を利用できないため、インターフェースと実装を紐づけるトークンを定義し、そのトークンを指定することで実装を登録・取得する必要があります。
import { InjectionToken, container, inject, injectable } from 'tsyringe';
type IUserRepository = {
findById(id: string): Promise<UserEntity>;
};
class UserRepository implements IUserRepository {
findById(id: string): Promise<UserEntity> {}
}
// なにかしらのトークンを作成して実装を登録・取得する。
const userRepositoryToken = Symbol();
container.register(userRepositoryToken as InjectionToken, UserRepository);
@injectable()
class FindUserUseCase {
constructor(@inject(userRepositoryToken) private userRepository: IUserRepository) {}
run(id: string): Promise<UserEntity> {
return this.userRepository.findById(id);
}
}
abstract class を使用した成功例 #
abstract class はランタイム時も残るためキーに使用することができます。そのためトークンを利用する必要がなくなり記述がシンプルになります。
import { InjectionToken, container, injectable } from 'tsyringe';
abstract class IUserRepository {
abstract findById(id: string): Promise<UserEntity>;
}
class UserRepository implements IUserRepository {
findById(id: string): Promise<UserEntity> {}
}
// IUserRepository はランタイム時にも残るため、キーとして利用できる。
container.register(IUserRepository as InjectionToken, UserRepository);
@injectable()
class FindUserUseCase {
constructor(private userRepository: IUserRepository) {}
run(id: string): Promise<UserEntity> {
return this.userRepository.findById(id);
}
}
FYI: #
Interface / Abstract class #
- What is the difference between interface and abstract class in Typescript?
TSyringe #
- TypeScript の DI、その貮:TSyringe
- tsyringe インターフェースによる依存性の注入