今っぽい感じのTypeScript向けDIライブラリを作った

好みを詰め込んだDIライブラリを作りました。usingが使えます。

ライブラリ名はGyakuと言います。依存性の逆転から取っています。

スターください!

@gyaku/diという名前でnpmにも公開しています。

特徴

gyakuはusingで破棄処理が書けて、asyncなファクトリ関数をサポートするDIライブラリです。

以下のように書けます。

import { setTimeout } from "node:timers/promises";

import { createRegistry } from "@gyaku/di";

const createGreeter = async ({ name }: { name: string }) => {
  await setTimeout(100);
  return {
    say: () => console.log(`hello, ${name}`),
    [Symbol.asyncDispose]: async () => {
      await setTimeout(100);
      console.log(`bye, ${name}`);
    },
  };
};

const registry = createRegistry()
  .value("name", "gyaku")
  .service("greeter", ["name"], createGreeter);

{
  await using services = await registry.resolve();
  services.greeter.say();
  // hello, gyaku
}
// bye, gyaku

typed-injectに大きな影響を受けていて、APIは近いものになっています。

差分としては次のような感じです。

関数をメインでサポート

InversifyJStsyringeなどの有名なDIライブラリではクラスを使うことが前提になっていたり、それをメインにドキュメントが書かれていますが、gyakuでは関数がメインになっています。

これはDIを使った書き方をしている時の、classである必要なくない?という気付きから来ています。

class UserService {
  constructor(private repository: UserRepository) {}

  getUser(id: string) {
    return this.repository.find(id);
  }
}

// ↓これでいい

function createUserService(repository: UserRepository) {
  return {
    getUser(id: string) {
      return repository.find(id);
    },
  };
}

クラスを使わないことで次のようなメリットがあります。

  • コンストラクタでは使えなかった非同期処理が書ける
  • 継承する/される可能性について考えなくてよくなる
  • thisが不要になり記述が簡潔になる

gyakuではDIライブラリでありながら珍しく非同期関数をサポートします。

アプリケーションの起動時に何らかの非同期処理の完了が必要な場合でも、DIの初期化処理に組み込むことができます。

usingが使える

多くのDIライブラリがそうであるように、gyakuでは起動処理だけでなく終了処理も面倒を見ます。

しかし既存のDIライブラリは、ライブラリから提供されているdisposeメソッドを使用して開発者自身がそのタイミングを制御する必要があります。

gyakuではusingを使用した破棄処理をサポートします。以下のように書けます。

function createDatabase(config: Config) {
  // 何らかのDBライブラリ
  const db = new Database(config.DATABASE_URL);
  Object.defineProperty(db, Symbol.asyncDispose, {
    value: async () => {
      await db.disconnect();
    },
  });
  return db;
}

こう書いてawait using付きのresolveを呼び出すだけで、disposeの呼び出しが不要でスコープを抜けた時に自動的に破棄処理が呼ばれます。

いつでもやめられる

gyakuではDIに関する知識を実装に書く必要がありません。そのためやめたくなった時にいつでもやめられます。

const registry = createRegistry()
  .value("name", "gyaku")
  .service("greeter", ["name"], createGreeter);

↑のようにコンテナを宣言するときだけ依存関係を書きます。ちなみにここで書いた依存関係は型チェックされるので、不正な依存を書くとエラーになります。

この時ファクトリ側を見ると引数がオブジェクトである以外ルールがありません。inject@injectable()も不要です。

const createGreeter = ({ name }: { name: string }) => {
  return {
    say: () => console.log(`hello, ${name}`),
  };
};

つまりgyakuをやめたくなったら手動でDIするなり他のDIライブラリに差し替えるだけです。非常に簡単にやめられます。

なぜ作ったか

NestJSが好きだけどきつかったから2週間でWebフレームワーク作った ( ZeltJS ) という記事を読んでライブラリを作ってみたくなったからです。

自分用にライブラリを作ったことは何度かありましたが、AIに指示しながらであれば自分では作ろうと思わない分野のものも作れるのではと思いました。

また、ちょうどDIライブラリであるtyped-injectを試していて、無視できるレベルであるものの若干変えたいところがあると感じていました。

AIと協力してライブラリを作るコツ

gyakuは全体で300行ほどの小さい実装で、これくらいであれば現状のAIなら何の工夫もなく狙った通りの実装を作ってくれます。

一方でいくつか気にした方が良いことがあります。

細かい仕様をAIと議論しておく

一見動くものができるのですが、その分細かい仕様を見落としがちです。早い段階でgrill-meスキルを使うなどして仕様を詰めていくと良いです。

例えばgyakuでは最初は依存関係の解決時に発生したエラーをAggregateErrorに詰めて投げていたのですが、これだと発生したエラーがどのファクトリから発生したものかが分からないようになっていました。これは後から修正しています。

AIと議論してユーザーがどうやってそのライブラリを使いたいかを想像し、それに見合ったAPIにできるようにしましょう。

AIに理解度テストを出させる

AIに実装させるとはいえ、可能な限りコードをレビューして全体を理解しておくべきです。

そこでやると良いのが「数行ずつ実装を解説して。理解度テストを出して、正解したら次の説明に進んで」などと指示をして、実装内容を理解できるまで説明させることです。

この理解度テストを出してもらうのが結構良く、ただ解説してもらうより頭に入ってくる気がします。

一方でこの理解度テストのクオリティに結構差があったと感じていて、選択肢より自由回答になるようにプロンプトで誘導した方がいいかもしれません。

おわり

AIを使って自分用のライブラリを作ってみました。このライブラリはこれから自分でも使ってみて少しずつ育てていこうと思います。

もし使ってみた方がいればフィードバック頂けると嬉しいです。