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

一般的にTypeScriptでDIをしようとするとクラス+デコレータを避けて通れません
しかし個人的には関数型っぽくコーディングするのが好きなのでクラスもデコレータも使わず関数だけでDIする方法を考えました
たった15行なので解説の前にコードを紹介します

```ts
type Deps<T extends Record<string, any>> = {
  [P in keyof T]: T[P] extends { _velona: boolean }
    ? (...args: Parameters<T[P]>) => ReturnType<T[P]>
    : T[P]
}

export const depend = <T extends Record<string, any>, U extends any[], V>(
  dependencies: T,
  cb: (deps: Deps<T>, ...args: U) => V
) => {
  const fn = (...args: U) => cb(dependencies, ...args)
  fn._velona = true
  fn.inject = (deps: Deps<T>) => (...args: U) => cb(deps, ...args)
  return fn
}
```

当然フロントでもバックでも使えます
「Velona(ヴェローナ)」と名付けてnpmにも公開済みです
https://github.com/frouriojs/velona

スターを押してもらえると嬉しいです！

## 基本的な使い方

上記のコードをコピペしてファイルを作るかnpmでインストールします

```bash
$ npm install velona
```

例えば、足し算をする関数があるとします

```ts:add.ts
export const add = (a: number, b: number) => a + b
```
このadd関数に依存する関数を定義します

```ts:parentFn.ts
import { add } from './add'

export const parentFn = (a: number, b: number, c: number) => add(a, b) * c

console.log(parentFn(2, 3, 4)) // 20
```

parentFnはこのままではテスト時に依存性（add関数）を別の関数に差し替えることができません
そこでVelona(冒頭の15行)を使います

```ts:parentFn.ts
import { depend } from 'velona'
import { add } from './add'

export const parentFn = depend(
  { add }, // 連想配列でデフォルトの依存性を注入する
  ({ add }, a: number, b: number, c: number) => add(a, b) * c // ここのaddは「注入された」ローカルスコープの関数
)

console.log(parentFn(2, 3, 4)) // 20
```

parentFnの呼び出し方は元と変わってません
dependを使って定義するときに第一引数に依存性を連想配列で渡し、第二引数の関数の最初の引数で受け取れます

これでDIでのテストが可能になりました
以下のコードはJestですが、どのフレームワークでも使い方は同じです

```ts:parentFn.spec.ts
import { parentFn } from './parentFn'

test('parentFnにDI出来るかどうかチェック', () => {
  const injectedParentFn = parentFn.inject({ add: (a, b) => a * b }) // 足し算を掛け算に変えてみる

  expect(parentFn(2, 3, 4)).toBe((2 + 3) * 4) // 20でPass、parentFnの結果は足し算のまま
  expect(injectedParentFn(2, 3, 4)).toBe(2 * 3 * 4) // 24でPass、parentFnの結果が掛け算に変わった
})
```

たったこれだけのことで、JavaScriptでよく使うコールバック関数にDIが出来るようになります

## コールバック関数にDIする

DOMのクリックイベントやExpressのミドルウェアなどコールバック関数が必要になる場面はたくさんあります

```ts
export const doSomething = () => {
  console.log('Hello workd!') // <- typo
}

setTimeout(doSomething, 1000)
```

例えば上の例だと、期待する文字列`Hello world!`をdoSomethingが出力するかどうかをテストするにはconsole.logを以下のようにspyOnする必要があります

```ts
import { doSomething } from './doSomething'

test('Hello world!が出力される', () => {
  const spy = jest.spyOn(console, 'log')

  doSomething()
  expect(console.log).toHaveBeenCalledWith('Hello world!') // Error!

  spy.mockRestore()
})
```

DIが出来るようにVelonaを使ってみます

```ts
import { depend } from 'velona'

export const doSomething = depend(
  { print: (message: string) => console.log(message) },
  ({ print }) => print('Hello workd!') // <- typo
)

setTimeout(doSomething, 1000)
```

テストコードはちょっと長くなりますね

```ts
import { doSomething } from './doSomething'

test('Hello world!が出力される', () => {
  let called
  const injectedDoSomething = doSomething.inject({
    print: message => {
      called = message
    }
  })

  injectedDoSomething()
  expect(called).toBe('Hello world!') // Error!
})
```

## 結合テストでネストした関数にDIする

parentFnから3階層下のadd関数を掛け算関数に差し替えてみます

```ts:add.ts
export const add = (a: number, b: number) => a + b
```

```ts:grandchild.ts
import { depend } from 'velona'
import { add } from './add'

export const grandchild = depend(
  { add },
  ({ add }, a: number, b: number) => add(a, b)
)
```

```ts:child.ts
import { depend } from 'velona'
import { grandchild } from './grandchild'

export const child = depend(
  { grandchild },
  ({ grandchild }, a: number, b: number, c: number) => grandchild(a, b) * c
)
```

```ts:parentFn.ts
import { depend } from 'velona'
import { child } from './child'

export const parentFn = depend(
  {
    child,
    print: (data: number) => alert(data) // ブラウザではalertが呼ばれるが、テスト環境のNode.jsにはないのでこれもDIが必要
  },
  ({ child, print }, a: number, b: number, c: number) => print(child(a, b, c))
)
```

ここまででparentFnの定義が完了です
3階層下にadd関数がいる状態

ブラウザ上では以下の通り普通の関数として使えます

```ts:index.ts
import { parentFn } from './parentFn'

parentFn(2, 3, 4) // alert(20)
```

テスト時に最下層のaddを差し替えるには愚直にinjectの入れ子を行います

```ts:parentFn.spec.ts
import { parentFn } from './parentFn'
import { child } from './child'
import { grandchild } from './grandchild'

const injectedFn = parentFn.inject({
  child: child.inject({
    grandchild: grandchild.inject({
      add: (a, b) => a * b // 最下層のaddを掛け算に変更
    })
  }),
  print: data => data // alertはJest/Node.js環境にはないので差し替える
})

expect(injectedFn(2, 3, 4)).toBe(2 * 3 * 4) // pass
```

結構な手間に見えますが、エディタがサポートしてくれるので見た目ほど大きな負担にはならないです

## もう一度15行のコードをおさらい

上記の機能全てがたったの15行で実現できます

```ts
type Deps<T extends Record<string, any>> = {
  [P in keyof T]: T[P] extends { _velona: boolean }
    ? (...args: Parameters<T[P]>) => ReturnType<T[P]>
    : T[P]
}

export const depend = <T extends Record<string, any>, U extends any[], V>(
  dependencies: T,
  cb: (deps: Deps<T>, ...args: U) => V
) => {
  const fn = (...args: U) => cb(dependencies, ...args)
  fn._velona = true
  fn.inject = (deps: Deps<T>) => (...args: U) => cb(deps, ...args)
  return fn
}
```

コピペしてもいいですが、Velonaをインストールすると共通概念として会話が成り立つのでおススメです
https://github.com/frouriojs/velona

面白いアイディアだなあと思ったらスターを押してもらえると嬉しいです！