TypeScriptで緩くDI的な事をしたい

目次

これは8月6日にZennに投稿した記事を移動させたものです。

何がしたいか

参照透過性、純粋関数、テストしやすい関数みたいな文脈で調べていて、関数が外部に依存している部分を自由に外から注入できるようにしたいと思った。

mockやspyOnを書くのがめんどくさくて嫌いで、その関数自身がDIの機能を持ってるとテストが書きやすくて楽そう。

Velona

デコレータを使わずコールバック関数に依存性を注入できるDIヘルパーをたった15行のTypeScriptで作った話

自分がしたい事をスマートに叶えてくれそうなのがあった。

が、これだとdependenciesの部分が関数の作成時に実行されてしまう🥲

import { depend } from 'velona'
 
export const myFunc = depend({
  logger: (() => {
    console.log('funcを使用するかに関わらず、このログは出力される')
 
    return console.log
  })()
}, ({ logger }, name: string) => {
  logger(name)
})

また、テストを書くときに以下のようにしてもその変数名がログに出てくれない。

import { describe, expect, it, vi } from "vitest"
import { myFunc } from "."
 
describe(myFunc, () => {
  it('should log name', () => {
    const mockLogger = vi.fn(() => {})
    const name = "user"
 
    const injected = myFunc.inject({
      logger: mockLogger
    })
 
    injected(name)
 
    expect(mockLogger).toHaveBeenCalledWith(name)
  })
})

出力されるログ

これのfnmyFuncとなってほしい。

 ✓ src/index.test.ts (1)
   ✓ fn (1)
     ✓ should log name

 Test Files  1 passed (1)
      Tests  1 passed (1)

これはdepend関数が内部で関数を作成していて、それの名前がfnとなっており、

const fn = (...args: U) => cb(dependencies, ...args)

Vitestはdescribeの第一引数に関数を渡されると、Function.nameを取得して表示しているため。

で、ついでに気になったので、寄り道して変数そのものの名前を取得する方法を調べてみた。

JavaScript 変数名と値を文字列として取得する

めんどくさ・・・案外こういうの難しいんだな。

開き直る

ライブラリを利用するといろいろとだるそうだったので、
コーディングの規約を設け、関数はこのように作る事にすればええか〜という事でこの話は終わりにした。

export const myFunc2 = (
  // 実行時に受け取る引数は全て第一引数に入れて、
  { name }: { name: string },
  // 依存関係が第二引数に存在する事を統一する
  { __deps: { logger = console.log } = {} } = {}
) => {
  logger(name)
}
import { describe, expect, it, vi } from "vitest"
import { myFunc2 } from "."
 
describe(myFunc2, () => {
  it('should log name', () => {
    const mockLogger = vi.fn(() => {})
    const name = "user"
 
    myFunc2({ name }, { __deps: { logger: mockLogger } })
 
    expect(mockLogger).toHaveBeenCalledWith(name)
  })
})

外部モジュールに依存するものは全て__deps内に入れる事で、テストでモックが簡単になるし、関数が何に依存しているか一目でわかるようになる。