TSyringeの使い方とコンテナ登録方法ごとの評価タイミングについて

2021.06.02

吉川@広島です。

TypeScriptのDIコンテナライブラリはInversifyJSとTSyringeが有名ですが、より機能がミニマムと思われる後者に興味が出たため使ってみました。

https://github.com/microsoft/tsyringe

一番メジャーなInversifyに比べるとTSyringeはやや情報が少ないのと、公式のREADMEもかなりあっさりとしていて使い始める際に戸惑いやすいと思いました。

本記事では、

  • 基本的な使い方
  • コンテナに登録したクラスのインスタンスをいつ生成しているか

を確認したので紹介します。

後者が気になった理由は、DIコンテナへの登録は遅延評価方式に寄せた方がアプリケーション起動時のパフォーマンスに有利に働きやすいと考えるためです。

環境

  • TSyringe v4.4.0

使い方

import 'reflect-metadata'
import { container, inject, injectable } from 'tsyringe'

interface IBar {
  greet(): void
}

class Bar implements IBar {
  constructor() {}

  greet(): void {
    console.log('Hello.')
  }
}


@injectable()
class Foo {
  // IBarをコンストラクタインジェクションする
  constructor(@inject('BAR') private readonly bar: IBar) {}

  greet(): void {
    this.bar.greet()
  }
}

// コンテナにクラスを登録
container.register('BAR', {
  useClass: Bar,
})

// コンテナにクラスを登録
container.register('FOO', {
  useClass: Foo,
})

// インスタンスを取得する
const foo = container.resolve<Foo>('FOO')
foo.greet()
# 出力結果
$ ts-node index.ts
Hello.
✨  Done in 1.58s.

IBar型のインスタンスをフィールドに持つFooクラス、という例です。

クラスの定義時にコンストラクタインジェクションする引数に @inject('TOKEN') デコレータを付けておきます。
'TOKEN' は例では文字列をそのまま渡していますが、実践では定数管理しておくのが良いと思います。

また、注意点として、DIコンテナから注入されるクラスにデコレータ @injectable() を付けてあげる必要があります。
@injectable() を書き忘れると、実行時に、

Error: TypeInfo not known for "Foo"

というエラーが発生します。

クラスの登録を container.register('TOKEN') で行い、 依存解決したインスタンスの取得を container.resolvce('TOKEN') で行います。

インスタンス生成のタイミング

コンテナへの登録方法は、ざっくり

  • useClass
  • useValue
  • useFactory

を押さえておけば良さそうです。

これらをどう使い分けるのかを判断するために、それぞれインスタンス生成のタイミングに注目して挙動を検証してみました。

まず、

class Foo {
  constructor() {
    console.log('constructing!')
  }
}

このようなFooクラスを用意します。

コンストラクタの中に console.log('constructing!') を記述しているので、インスタンス生成されていれば constructing! が出力されるはず、というわけです。

手元の端末上でts-nodeで実行して検証してみます。

useClassの場合

import 'reflect-metadata'
import { container } from 'tsyringe'

class Foo {
  constructor() {
    console.log('constructing!')
  }
}

container.register('FOO_USE_CLASS', {
  useClass: Foo,
})
# 出力結果
$ ts-node index.ts
✨  Done in 1.65s.

コンストラクタは走っていません。

下のように resolve 処理を追記します。

container.register('FOO_USE_CLASS', {
  useClass: Foo,
})

container.resolve('FOO_USE_CLASS') // 追加
# 出力結果
$ ts-node index.ts
constructing!
✨  Done in 1.56s.

resolve 呼び出し時にインスタンス生成するようです。

useValueの場合

import 'reflect-metadata'
import { container, inject, injectable } from 'tsyringe'

class Foo {
  constructor() {
    console.log('constructing!')
  }
}

container.register('FOO_USE_VALUE', {
  useValue: new Foo(),
})
# 出力結果
$ ts-node index.ts
constructing!
✨  Done in 1.58s.

インスタンス生成はコンテナへの登録時に行うようです。

useFactoryの場合

import 'reflect-metadata'
import { container } from 'tsyringe'

class Foo {
  constructor() {
    console.log('constructing!')
  }
}

container.register('FOO_USE_FACTORY', {
  useFactory: () => new Foo(),
})
# 出力結果
$ ts-node index.ts
✨  Done in 1.52s.

コンストラクタは走っていません。

下のように resolve 処理を追記します。

container.register('FOO_USE_FACTORY', {
  useFactory: () => new Foo(),
})
container.resolve('FOO_USE_FACTORY') // 追加
$ ts-node index.ts
constructing!
✨  Done in 1.73s.

resolve 呼び出し時にインスタンス生成するようです。

まとめ

  • クラスの登録は useClass
    • useClassresolve 時に遅延評価的にインスタンス生成しそれを取得する
  • それ以外のプリミティブな値など (例: 文字列、数値) は useValue useFactory
    • 静的な値を登録したい場合は useValue
    • 動的に値を決定したい場合は useFactory
    • resolve 時に遅延評価的に取得したい場合は useFactory

と使い分ければ良いのかな、と思いました。

本文紹介以外の参考記事