第7章 依存逆転(DIP)をTSで体験:中心→外側importを断つ🔄🚫
この章のゴール🎯
「中心(業務ルール・ユースケース)」が、DB/HTTP/外部SDKみたいな外側の詳細を import しない状態を作れるようになること💪
そして、実装(外側)を差し替えられる気持ちよさを体験します😆🔁
ちなみに本日時点では TypeScript の最新は 5.9.x(npm上で 5.9.3)です🧡 (npmjs.com)
(5.9では import defer みたいな“依存の実行タイミング”にも関わる機能が入ってますが、今日はもっと根本の「依存の向き」を直します🧭) (TypeScript)
1) まず「依存が逆」ってどういう事故?😱🌀
ありがちな事故はこれ👇
-
usecase/RegisterUser.tsがimport { PrismaClient } from "@prisma/client"みたいに DBライブラリを直import -
その結果…
- DBの都合でユースケースが壊れる💥
- テストがつらい(DB必要)😭
- “中心”が“外側”に支配される😵💫
イメージ図🗺️(ダメな矢印)
- 中心 → 外側(詳細)に依存しちゃってる
2) ダメな例(わざと)🚫🧪
// src/app/RegisterUser.ts ← 本当は中心側に置きたいのに…
import { PrismaClient } from "@prisma/client"; // ← 外側の詳細を直import 😱
export class RegisterUser {
private prisma = new PrismaClient();
async execute(email: string) {
// DBに直接ベタ書き
const user = await this.prisma.user.create({ data: { email } });
return user;
}
}
これ、書きやすいけど「中心が外側に引っ張られる」典型です🥲
3) DIP(依存逆転)の考え方🔄🧠✨

合言葉はこれ🗣️💡
「中心は“実装”じゃなく“能力(契約)”だけ知る」
- 中心:
UserRepositoryっていう 契約(interface) だけを見る👀 - 外側:DB版やインメモリ版を 契約に従って実装 する🧰
- 最後に:外側の実装を中心に 差し込む(注入) 🔁
イメージ図(良い矢印)🧅✨
- 中心(UseCase) → 契約(interface)
- 外側(DB/HTTP) → 契約(interface)を実装
つまり、中心が外側を見ない。これが勝ち筋です🏆💕
4) TSでやってみよう:最小ステップでDIP✅
ここから「リファクタ手順」をそのまま真似すればOKです😊
Step 1: 契約(interface)を中心側に置く📜🧡
// src/domain/UserRepository.ts
export interface UserRepository {
create(email: string): Promise<{ id: string; email: string }>;
}
ポイント🎀
IUserRepositoryみたいな “Iプレフィックス” は好み(どっちでもOK)- 大事なのは 「中心が欲しい能力」だけを書くこと✨
Step 2: ユースケースは契約だけを見る👀✨
// src/domain/RegisterUser.ts
import type { UserRepository } from "./UserRepository";
export class RegisterUser {
constructor(private readonly users: UserRepository) {}
async execute(email: string) {
// 中心は「作ってね」しか言わない
const user = await this.users.create(email);
return user;
}
}
ここでの勝利条件🏁
RegisterUser.tsに DBライブラリのimportが一切ない 🙆♀️✨
Step 3: 外側で実装する(DB版)🧰🗄️
(例として “Prismaっぽい” 実装にしてます。あなたのDB事情に置き換えてOK🙆♀️)
// src/adapters/db/DbUserRepository.ts
import type { UserRepository } from "../../domain/UserRepository";
// ※ここでは例としてDBクライアントがある想定
type DbClient = {
user: { create: (args: { data: { email: string } }) => Promise<{ id: string; email: string }> };
};
export class DbUserRepository implements UserRepository {
constructor(private readonly db: DbClient) {}
async create(email: string) {
return this.db.user.create({ data: { email } });
}
}
ポイント🎯
- DBの詳細(クライアントやSQL)は adapters側に閉じ込める 🔒✨
- 中心には一切漏らさない😌
Step 4: 外側で実装する(インメモリ版)🧠📦
テストやローカル動作用に超便利です😆
// src/adapters/memory/InMemoryUserRepository.ts
import type { UserRepository } from "../../domain/UserRepository";
export class InMemoryUserRepository implements UserRepository {
private seq = 0;
private store = new Map<string, { id: string; email: string }>();
async create(email: string) {
const id = String(++this.seq);
const user = { id, email };
this.store.set(id, user);
return user;
}
}
Step 5: いったん“仮の入口”で差し込む🔁🚪
(組み立て場所の本命は次章でやるやつ✨ ここは最小でOK)
// src/main.ts
import { RegisterUser } from "./domain/RegisterUser";
import { InMemoryUserRepository } from "./adapters/memory/InMemoryUserRepository";
async function main() {
const users = new InMemoryUserRepository();
const registerUser = new RegisterUser(users);
const created = await registerUser.execute("hello@example.com");
console.log(created);
}
main();
ここまでで、中心が外側importしてない状態が完成🎉🎉🎉
5) 「中心→外側importしてる」を見つけるコツ🔎🕵️♀️
VS Codeでこれやると早いです💨
-
src/domainやsrc/appで検索🔍from "@prisma/とかfrom "axios"とかfrom "firebase"とか
-
見つかったら、そのファイルは “中心なのに外側を見てる” 可能性大⚠️
6) よくあるつまずき(ここ超大事)🥺🧯
つまずき①:interfaceって実行時に無いよね?😳
そう!TypeScriptの interface/type は コンパイル後に消えます(実行時の型情報にはならない)ので、
「DIコンテナが勝手に型から解決する」みたいなのは、そのままだとできません🙅♀️
もし “デコレータ+メタデータ” 系でDIしたい場合は emitDecoratorMetadata などの世界になります(今回は手動DIでOK!) (Zenn)
つまずき②:契約を細かくしすぎて迷子🌀
最初は 「ユースケースが本当に欲しい操作」だけ に絞るのがコツ✂️✨ (増やすのは後でいくらでもできるよ😊)
つまずき③:中心にDTO/DB型が混ざる📦😵
中心は ドメインの言葉で話すのが基本! DBの行型やAPIレスポンス型は “境界” で変換する(第10章あたりで気持ちよく整理するやつ)🌈
7) ミニ演習(手を動かすやつ)👩💻🔥

演習A:DB版 ↔ インメモリ版を差し替えてみよう🔁✨
main.tsのnew InMemoryUserRepository()をnew DbUserRepository(db)に差し替える- ユースケース側が一行も変わらないのを確認👀🎉
演習B:「Clock」も契約にしてみよう⏰🧡
Clock契約を作るSystemClock(本物の日時)とFixedClock(テスト固定)を作る → テストが一気にラクになります😆
演習C:禁止importチェック✅
src/domainにaxiosやprismaを import してないか検索 → 0件なら勝ち🏆✨
8) AI(Copilot/Codex)に頼むと爆速なところ🤖💨
そのままコピペで使える指示例だよ🪄
依存違反を見つける🕵️♀️
- 「
src/domain配下のファイルから、外部ライブラリ(DB/HTTP/UI)の import を検出して一覧にして。修正方針も添えて」
契約の抽出を手伝わせる📜
- 「このユースケースが実際に使っているDB操作を最小の
UserRepositoryinterface にして。メソッド名と戻り値の型も提案して」
インメモリ実装を作らせる🧠
- 「
UserRepositoryを満たすInMemoryUserRepositoryを作って。連番IDでOK。テスト用途で使いたい」
テストも作らせる🧪
- 「Vitestで
RegisterUserのテストを書いて。インメモリ実装を注入して、DB無しで動くことを確認したい」
9) 章末チェックリスト✅✨
- ユースケース(中心)がDB/HTTP/SDKを
importしてない🚫 - 中心は
interface/typeの契約だけ見てる👀 - 外側に 実装が閉じ込められてる🔒
- インメモリ版に差し替えできた🔁🎉
- テストが “外部無し” で回せる未来が見えた🌈
おまけ:いまのTypeScriptの動き(超ざっくり)📰✨
- 現在の最新は 5.9.x(5.9.3)で、5.9では
import deferが入っています。 (npmjs.com) - さらに TypeScript 6.x / 7.x に向けた動きも進んでいて、6.0は2026年初頭を示唆する話も出ています。 (GitHub)
- TS 7 “ネイティブ版” のプレビュー情報も出てます(ビルド高速化系の流れ)。 (Microsoft Developer)
でも!DIPはTSのバージョンが変わってもずっと効く基礎体力なので、ここを押さえるとめちゃ強いです💪💖
次の第8章では、この「差し込み場所」を ちゃんと“組み立て専用の場所”に隔離して、さらに気持ちよくします🏗️✨