TypeScript:DI コンテナ使用時に抽象クラス(abstract class)をインターフェースとして利用する

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 #

TSyringe #