TypeScriptのDIコンテナライブラリ InversifyJSとTsyringeの基本的な使い方を比較してみた

TypeScriptのDIコンテナライブラリ InversifyJSとTsyringeの基本的な使い方を比較してみた

Clock Icon2024.08.25

リテールアプリ共創部のるおんです。先日 TypeScript を用いたバックエンドの初期開発においてDIコンテナを実装する際にどのライブラリを使用するかを検討する機会がありました。その際、TypeScriptでDIを実現するためのライブラリのうち、よく使われているライブラリである、TsyringeInversifyJS の基本的な使い方を比較したので共有したいと思います。

本記事で紹介すること/紹介しないこと

本記事では同じ機能を実現するコードを、TsyringeとInversifyJSのそれぞれのコードを比較しながら解説します。
DI(依存性注入)の基本的な考え方や、DIコンテナとはなどは解説しません。
DIについてはこちらの記事が大変参考になりました。今回はこのブログで紹介されているTsyrigneのコードを、InversifyJSで書いた場合と比較してみたいと思います。
https://zenn.dev/chida/articles/1f7df8f2beb6b6

DIコンテナライブラリ

まずそれぞれのライブラリについて

Tsyringe

Tsyringeは、Microsoftが開発したDIコンテナライブラリです。
公式によると、

A lightweight dependency injection container for TypeScript/JavaScript for constructor injection.

とのことで、InversifyJSに比べ、よりシンプルで軽量なDIコンテナライブラリです。デコレータを使用して依存関係を定義し、自動的に依存関係を解決します。

InversifyJS

InversifyJSは、より高度な機能を持つDIコンテナライブラリです。
公式によると、

A powerful and lightweight inversion of control container
for JavaScript & Node.js apps powered by TypeScript.

とのことで、 パワフルかつ軽量なDIコンテナライブラリです。
TypeScriptのDIコンテナライブラリについて検索すると、このライブラリがよく使用されている印象です。また、Githubのスター数や使用率もこちらの方が多いです。

Tsyringeと同様にデコレータを使用しますが、より細かい設定が可能かつ多機能で、大規模なアプリケーションにも適しています。

使用率の比較

2024年8月25日時点では、InversifyJSはTsyringeのおよそ2倍ほどダウンロードされており、GitHubのstar数も2倍以上になっています。
また、InversifyJSは公式ドキュメントが用意されていますが、TsyringeはGithubリポジトリのREADMEがその役割を果たしています。
スクリーンショット 2024-08-25 15.01.33

基本的な使用方法の比較

それでは、両ライブラリの基本的な使い方を比較してみましょう。
繰り返しになりますが、本記事ではライブラリのコード比較に徹し、DIの解説や必要性などは解説いたしません。
先ほど紹介したこちらの記事が個人的には大変参考になりました。こちらの記事ではTsyringeの書き方についてのみ記述されていたので、そのコードをお借りして、InversifyJSでも同様に書いてみて比較していきたいと思います。
以下の3つのファイルを中心に見ていきます。

  • database.ts
  • user.ts
  • index.ts

セットアップ

まずはライブラリのインストールです。DIコンテナを実装するには、両ライブラリでreflect-metadataをインストールする必要があります。これにより関数が持つメタ情報を扱えるようになります。

bash
npm install reflect-metadata

次に各ライブラリをinstallします。

bash
// inversifyの場合
npm install inversify 

// Tsyringeの場合
npm install tsyringe

次にtsconfig.jsonを修正してください。これにより、デコレーター機能と、メタデータ取得機能が使えるようになります。

tsconfig.json
{
  'compilerOptions': {
    'target': 'ES5',
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,   
  }
}

user.ts

まずは、依存関係を受け取るクラスであるUserクラスを定義します。UserクラスにはIDatabaseインターフェースを実装したオブジェクトが依存注入されます。
以下に各ライブラリで書いてみます。
Tsyringe:

user.ts
import { inject, injectable } from "tsyringe";

export interface IDatabase {
  saveUser: (user: User) => void;
}

@injectable()
export default class User {
  userId: number = 0
  userName: string = "taro"

  constructor(
    @inject("IDatabase") private database: IDatabase) { }

  saveUser() {
    if (this.userId) {
      this.database.saveUser(this)
    }
  }
}

InversifyJS:

types.ts
export const TYPES = {
  IDatabase: Symbol.for("IDatabase"),
  User: Symbol.for("User")
}
user.ts
import { inject, injectable } from "inversify";
import { TYPES } from "./types"

export interface IDatabase {
  saveUser: (user: User) => void;
}

@injectable()
export default class User {
  userId: number = 0
  userName: string = "taro"

  constructor(
    @inject(TYPES.IDatabase) private database: IDatabase) { }

  saveUser() {
    if (this.userId) {
      this.database.saveUser(this)
    }
  }
}

はい、user.tsファイルに関してはほとんど同じです。両方とも依存注入可能なクラスには@injectable()デコレーターを使用し、@injectでinterfaceへの依存を注入しています。
Tsyringeでは、依存関係を文字列で指定します(@inject("IDatabase"))。
InversifyJSに関しては、@injectの引数にtypesファイルで別途定義した依存関係の識別子を使用するようにしています。(@inject(TYPES.IDatabase))
これは、公式ドキュメントによると、

PLEASE MAKE SURE TO PLACE THIS TYPES DECLARATION IN A SEPARATE FILE. (see bug #1455)
(省略)
Note: It is recommended to use Symbols but InversifyJS also support the usage of Classes and string literals

というようにtypesファイルを別に使用するよう強調されています。
また、その識別子にはクラスや文字列でも可能ですが、Symboleを使用することが推奨されています。
Tsyringeの実用においても、識別子のファイルを別途設けるのもありだと思います。

database.ts

次に、Userクラスに注入される具体的な実装であるDatabaseクラスを定義します。このクラスはIDatabaseインターフェースを実装しています。
Tsyringe:

database.ts
import User, { IDatabase } from "./user";

export default class Database implements IDatabase {
  saveUser(user: User) {
    console.log(`Saved ${user.userName}! with tsyringe`);
  }
}

InversifyJS:

database.ts
import { injectable } from "inversify";
import User, { IDatabase } from "./user";

@injectable()
export default class Database implements IDatabase {
  saveUser(user: User) {
    console.log(`Saved ${user.userName}! with InversifyJS`);
  }
}

こちらもほとんど同じですが、
InversifyJSでは、Userクラスに注入される実装クラスであるDatabaseクラスにも@injectable()デコレータを使用して注入可能にマークする必要があります。 Tsyringeでは、この例ではデコレータは不要です。

依存関係の解決

最後に、DIコンテナの設定と使用方法を見てみましょう。
Tsyringe:

diContainer.ts
import 'reflect-metadata'
import { container } from "tsyringe";
import Database from "./database";
import User from "./user";

container.register("IDatabase", { useClass: Database });
index.ts
export const user = container.resolve(User)

user.userId = 100
user.userName = "hanako"
user.saveUser()
// => Saved hanako! with tsyringe

InversifyJS:

inversify.config.ts
import { Container } from "inversify";
import "reflect-metadata";
import Database from "./database";
import { TYPES } from "./types";
import User from "./user";

const container = new Container()

container.bind<Database>(TYPES.IDatabase).to(Database)
container.bind<User>(TYPES.User).to(User)
index.ts
const user = container.get<User>(TYPES.User)

user.userId = 100
user.userName = "hanako"
user.saveUser()
// => Saved hanako! with inversify

InversifyJSではContainerクラスを初期化する必要があります。また、公式によるとinversify.config.tsというファイル名のファイルにおいてコンテナの作成と設定を行うことが推奨されています。Tsyringeは適当にdiContainer.tsとして作成しました。

主な違いについて、
依存関係の登録:

インスタンスの取得:

登録の範囲:

  • Tsyringeでは、@injectable()デコレータが付いたクラス(この場合はUser)は自動的にDIコンテナに登録されるため、Userクラスの明示的な登録が不要です。インターフェース(IDatabase)と具体的な実装(Database)の紐付けのみを行っています。
  • InversifyJSでは、すべての依存関係(DatabaseとUser)を明示的に登録する必要があります。そのため、Userクラスもbindして登録する必要があります。

おわりに

どうでしたでしょうか。今回はTsyringeとInversifyJSの簡単なコードを見てみて、記述方法の違いを比較してみました。実際に書いてみてTsyringeの方が記述するコードや必要なファイルがInversifyJSに比べ少なく、直感的に感じました。一方で、InversifyJSの方がコード量が多い分より多機能で、大規模なプロジェクトや複雑な依存関係を持つアプリケーションに適しているようです。
今回二つのライブラリを比較することで、それぞれのライブラリの使い方を理解することができました。
参考になれば幸いです。

参考

InversifyJS
Tsyringe
TypeScriptのDIとTsyringeについて

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.