---
title: "tsyringe 套件 - Clean Architecture in TypeScript"
date: 2025-07-25T00:00:00+08:00
publishDate: 2025-07-25T00:00:00+08:00
lastmod: 2025-11-21T10:38:12+08:00
tags: ["TypeScript","Clean Architecture","架構","經驗","AI","tsyringe","Dependency Injection"]
series: "clean-architecture-in-ts"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/07/25/clean-architecture-in-ts-tsyringe-package/"
language: "zh-tw"
---



[tsyringe](https://github.com/microsoft/tsyringe) 是微軟維護的一款輕量依賴注入（DI，Dependency Injection）容器，可以幫助我們很輕鬆的管理物件的依賴注入，在 Clean Architecture 的實踐中我們會將許多物件細分職責，此時有依賴注入的機制可以幫助我們在測試、開發上更加順手。

<!--more-->

## 簡潔{#concise}

在發現 tsyringe 前，我大多使用 [InversifyJS](https://inversify.io/) 來處理依賴注入，然而 InversifyJS 的前置準備並不算容易，改用 tsyringe 後只需要很簡單的加上 `@injectable()` 就可以解決大多數的應用情境。

> 撰寫本文時 InversifyJS 的設計也變得相對簡單不少，如果需要更複雜的功能可以考慮使用 InversifyJS

舉例來說，我們有一個 `KvConversationRepository` 用來儲存使用者的對話，可以像下面這樣進行定義。

```ts
@injectable()
class KvConversationRepository {
	constructor(@inject(KvStore) private readonly kv: KVNamespace) {}

    // ...
}
```

當我們需要使用時，則只需要如下的實作。

```ts
const repository = container.resolve(KvConversationRepository)
```

這是一個非常常見的依賴注入實作方式，tsyringe 可以幫我們自動找到 `KvStore` 並且完成 `KvConversationRepository` 的實例化。

## 可測試性{#testability}

使用依賴注入可以很好的提高專案的可測試性，尤其是現在大多數語言模型收費仍然不便宜，也沒有一個很好的方法可以控制輸出。

舉例來說，我們有一個會呼叫語言模型的情境。

```ts
class Chat {
  constructor(private readonly model: LlmModel) {}

  public async execute(message: string): Promise<string> {
    // ...
	const res = await this.model.chat(message)
	return res.text
  }
}
```

雖然我們設計了依賴注入機制，但會需要在每個測試都重新建立假的測試物件，使用依賴注入套件或者框架，就可以透過覆蓋原本註冊的物件來達到我們的目的。

```ts
container.register(
	LlmModel,
	{ 
		useFactory: (c) => { return new MockLlmModel() }
	}
)
```

透過這個技巧，我們就不用在每個測試案例重新產生 `MockLLmModel` 物件，而是自動的在需要時產生新的物件。

## Serverless 友善{#serverless-friendly}

如果是在非 Serverless 環境，我們仍能透過像是加入 `setup.vitest.ts` 這類方式預先定義好依賴，但是 Serverless 每一次請求都是獨立的，這能很好的提高拓展性，卻也讓我們無法共用預先定義的依賴。

以 Cloudflare 為例子

```ts
export default {
	async fetch(request, env, ctx) {
		// KVNamespace => env.KV
		return new Response('Hello World!');
	},

};
```

如果我們要拿到 `env.KV` 這個 `KVNamespace` 的 Binding（綁定）就需要等到我們接受到請求，這樣就不能在 `export default` 前先將 Repository 這類跟資料庫相關的物件產生出來，必須每個請求都處理。

受限於這樣的特性，我們就很難再設定測試環境時完成定義，也沒辦法讓所有請求共用相同的實例，那麼在測試跟開發階段都會受到很大的限制。

然而，我們可以利用 tsyringe 的 `useFactory` 特性，動態的處理這個行為。

```ts
container.register(
	KvConversationRepository,
	{
		useFactory: (c) => {
			const kv = c.resolve(KVNamespace)
			return new KvConversationRepository(kv)
		}
	}
)

export default {
	async fetch(request, env, ctx) {
		container.register(KVNamespace, { useValue: env.KV })

		// ...

		return routes.handle(request, env, ctx)
	},

};
```

依照上述的設計，`KvConversationRepository` 並不會一開始就被初始化，而是在被使用到時才呼叫，那麼就可以在 `KVNamespace` 還不存在之前被定義（`@injectable()` 也有類似的效果）

這樣我們就可以在開發跟測試中獲得更多設計上的彈性，而且每次都會產生新物件實例的特性也一定程度上可以避免副作用（Side Effect）的發生，即使我們在設計上就需要避免。

> Cloudflare 在最近的更新已經允許用 `import { env } from "cloudflare:workers";` 來直接取用 `env` 而不需要透過 `fetch()` 這類 Handler 來獲取。
