2021年9月12日 • ☕️ 4 min read

TypeScriptのDIライブラリの検証を続けていきたいです。今回はTypeDIを使ってみようと思います。

※ DI(dependency inject依存性注入)の説明は割愛します。必要があればwikipediaをご覧ください。

シリーズ一覧

  1. TypeScriptのDI、その壱:InversifyJS
  2. TypeScriptのDI、その貮:TSyringe
  3. TypeScriptのDI、その弎:TypeDI ←← ここ
  4. TypeScriptのDI、その肆:nestjs
  5. C# .NetFrameworkのDI

目的

C# .netと同じように、interfaceconstructorのDIが望ましいです。具体的なやり方と理由は最後のその5で説明します。

TypeDI

decorator推進するコミュニティが作ったDIライブラリとなります。InversifyJS並みの古さとなります。他のDIライブラリと同じように、JavaScriptとTypeScript両方サポートしています。classベースのconstructor injectionはtokenの定義が不要となり、とにかく簡単です。

⚠️⚠️⚠️:ただ、バージョンは1に達していなく、ドキュメントサイトも中身のないページいくつがあり、さらにREADME.mdに載せているサンプル自体が1年以上間違っているそうで、採用する時要注意です。

https://github.com/typestack/typedi

実装方法

interfaceと実装classの実装方法はInversifyJSとほぼ同じとなります。

  1. まずinterfaceと実装部分を定義し、実装部分にdecoratorを使ってDIできるclassを明記します。
ProjectService.ts
import { Service } from 'typedi';

export interface IProjectService {
  getProject(id: string): Promise<Project>;
}

@Service()export default class ProjectService implements IProjectService {
  async getProject(id: string): Promise<Project> {
    console.log(`getProject: ${id}`);
    return new Project(id, `project_name_${id}`);
  }
}
  1. TypeScriptのinterfaceはC#と違って、reflectionでinterfaceからpropertiesなどを取れないため、interfaceでinjectできるように、bindingはtoken形式で行わなければいけません。まずtokenを定義しましょう。
types.ts
// 残念ながら、TypeDIがSymbolサポートしていないため、文字列を使用する
export const TYPES_STRING = {
  EmployeeService: 'EmployeeService',
  ProjectService: 'ProjectService',
}
  1. そして、inject先のclassのconstructorにDIします。

簡単にいうと、ここのtoken(TYPES_STRING.EmployeeService -> String)はキーとして、containerから実装classを特定し、instance化しています。

EmployeeService.ts
import { Inject, Service } from 'typedi';
import { IProjectService } from './ProjectService';

export interface IEmployeeService {
  getEmployee(id: number): Promise<Employee>;
}

// 他のclassにinjectできるようにProjectServiceと同じように`@Service()`を追加する
@Service()export default class EmployeeService implements IEmployeeService {
  public constructor(
    // `@Inject(TYPES.ProjectService)`はtoken形式で`IProjectService`の実装classをinjectする
    @Inject(TYPES_STRING.ProjectService) private _projectService: IProjectService,  ) {}

  async getEmployee(id: number): Promise<Employee> {
    console.log(`getEmployee: ${id}`);

    // inject成功!!使える!!
    const project = await this._projectService.getProject(`proj_id_${id + 1}`);

    return new Employee(
      id,
      `dummy_name_${id}`,
      project,
    );
  }
}
  1. interfaceと実装classのbindingを行います。

ステップ3でinjectしたIProjectServiceはinterfaceで、どの実装classを指しているか指示する必要があります。

typedi.config.js
import { Container } from 'typedi';
import ProjectService from './services/ProjectService';

// @Injectを使う部分のみbindingを設定すればいい、classベースのconstructor injectionは自動で行う
Container.set(TYPES_STRING.ProjectService, new ProjectService());
  1. トップclassにtypedi.config.jsをimportすれば完了となります。
app.ts
// metadataとるためのpolyfill
import 'reflect-metadata';
import './typedi.config';
import { Container } from 'typedi';
import EmployeeService from './services/EmployeeService';

// bindingしなくても、@Service()でdecorate済みのclassなら自動bindingしてくれる
const employeeService = Container.get(EmployeeService);
async function main() {
  const employee = await employeeService.getEmployee(12);
  console.log(JSON.stringify(employee, null, 2));
}

void main();

感想

TypeScriptの各DIライブラリと同じ問題を抱えている軽量なDIライブラリとなります。

明記的にbindingしなくてもcontainerからinstance化できるのが楽だが、自由度が高すぎると思います。ドキュメントの質とコミュニティの活発度を考える(v0.8.0 -> v0.9.0 2年ほどかかった)と、上位互換のTSyringe或いはInversifyJSを使った方が無難な気がします。

サンプルプロジェクト:

https://github.com/thundermiracle/di-typescript/tree/master/projects/typedi-token


関連投稿

TypeScriptのDI、その肆:nestjs

2021年9月12日

TypeScriptのDI、その貮:TSyringe

2021年9月11日

ThunderMiracle

Blog part of ThunderMiracle.com