第14章 Composition Root:newとimportの集中管理📍🧹
今日のゴール🎯✨
- 「Composition Rootって何?」を自分の言葉で説明できる😊
newと「環境依存import(browser / node)」を1か所に寄せる理由がわかる📦- 依存の向きルール(domain/app → infra をimportしない)を守れる🚫➡️
1) Composition Rootってなに?🧩

**アプリの“組み立て工場”**だよ🏭✨ 「どの実装を使う?」「どの順で作る?」「どこに注入する?」を、起動点(main/entry)でぜんぶ決める場所。
TypeScriptは型が実行時に消えるから、C#みたいに「型だけで自動解決〜!」がやりにくい場面が多いよね👻 だからこそ、手で組み立てる場所(Composition Root)を綺麗に作るのが超大事💎
※2026年1月時点で TypeScript は 5.9.3 が最新安定版として配布されてるよ📌 (GitHub)
2) なんで「new と import を1か所」に集めるの?🤔💡
✅ うれしいこと 3つ🎁
-
変更がラク🔄 保存先を
localStorage→APIに変えても、直すのは“組み立て側”だけになりやすい✨ -
テストがラク🧪 本番は
SystemClock、テストはFakeClockみたいに差し替え簡単⏰ -
import地雷を踏みにくい💥 ブラウザ専用 / Node専用の import を混ぜると壊れがち😵 だから 環境依存importは entry に閉じ込めるのが安全!
(ちなみに Node.js は 2026/1 時点で v24 が Active LTS、v22 が Maintenance LTS などの扱いだよ📌 (Node.js))
3) まずは“依存の向き”ルールを1本だけ決めよう📏✨
ここだけ守ると、世界が平和になる🕊️💕
domain(コアロジック)とapp(ユースケース)は、infraをimportしない🚫infraは外部I/O(storage / fetch / fs / env)担当🌐🗄️entry(Composition Root)だけが infraもappもimportしてOK🙆♀️
4) ミニ例:Todoを追加する(Clock/Logger/Repo をDI)📝💉
フォルダ構成(おすすめの最小形)🗂️✨
src/
domain/
todo.ts
ports.ts
app/
addTodo.ts
infra/
systemClock.ts
consoleLogger.ts
localStorageTodoRepo.ts
memoryTodoRepo.ts
entry/
webMain.ts ← Composition Root(ブラウザ用)
testMain.ts ← (任意)結合テスト用の組み立て
5) コードで作ってみよう💻🌸
(A) domain:契約(Port)を定義📜
// src/domain/ports.ts
export type Todo = {
id: string;
title: string;
createdAt: Date;
};
export interface Clock {
now(): Date;
}
export interface Logger {
info(message: string): void;
}
export interface TodoRepo {
add(todo: Todo): Promise<void>;
}
(B) app:ユースケース(外から渡された依存だけ使う)🎯
// src/app/addTodo.ts
import { Clock, Logger, Todo, TodoRepo } from "../domain/ports";
export class AddTodo {
constructor(
private readonly repo: TodoRepo,
private readonly clock: Clock,
private readonly logger: Logger,
) {}
async execute(title: string): Promise<Todo> {
const todo: Todo = {
id: crypto.randomUUID(),
title,
createdAt: this.clock.now(),
};
await this.repo.add(todo);
this.logger.info(`Todo added: ${todo.id}`);
return todo;
}
}
ポイント💡:
AddTodoはlocalStorageもconsoleもfetchも知らない🙈 ただ「契約(interface)」だけを見る🧡(ここがDIの美味しいところ!)
(C) infra:実装たち(外部I/Oの世界)🌐🗄️
// src/infra/systemClock.ts
import { Clock } from "../domain/ports";
export class SystemClock implements Clock {
now(): Date {
return new Date();
}
}
// src/infra/consoleLogger.ts
import { Logger } from "../domain/ports";
export class ConsoleLogger implements Logger {
info(message: string): void {
console.log(message);
}
}
// src/infra/localStorageTodoRepo.ts
import { Todo, TodoRepo } from "../domain/ports";
export class LocalStorageTodoRepo implements TodoRepo {
constructor(private readonly storage: Storage) {}
async add(todo: Todo): Promise<void> {
const key = "todos";
const raw = this.storage.getItem(key);
const list: any[] = raw ? JSON.parse(raw) : [];
list.push({ ...todo, createdAt: todo.createdAt.toISOString() });
this.storage.setItem(key, JSON.stringify(list));
}
}
テスト用のメモリ実装もあると超便利🧸
// src/infra/memoryTodoRepo.ts
import { Todo, TodoRepo } from "../domain/ports";
export class MemoryTodoRepo implements TodoRepo {
public readonly items: Todo[] = [];
async add(todo: Todo): Promise<void> {
this.items.push(todo);
}
}
6) ここが本題!Composition Root(組み立て)📍✨
(D) entry:ブラウザ用 main(new と import の集中)🪟🚀
// src/entry/webMain.ts
import { AddTodo } from "../app/addTodo";
import { ConsoleLogger } from "../infra/consoleLogger";
import { SystemClock } from "../infra/systemClock";
import { LocalStorageTodoRepo } from "../infra/localStorageTodoRepo";
export function bootstrap() {
// ✅ ここが Composition Root:new を集約
const repo = new LocalStorageTodoRepo(window.localStorage);
const clock = new SystemClock();
const logger = new ConsoleLogger();
const addTodo = new AddTodo(repo, clock, logger);
return { addTodo };
}
使う側(UI)は “組み立て” を知らないでOK😊
// 例:どこかのUI側
import { bootstrap } from "./entry/webMain";
const { addTodo } = bootstrap();
await addTodo.execute("レポート提出する📄✨");
7) Composition Rootがデカくなってきたら?🐘💦(太りすぎ対策)
✅ 合言葉:「new は entry、でも entry は“薄く”」🍃
おすすめの分割パターン👇
(1) “機能ごとのwire”に分ける
wireTodo.ts(Todo関連の組み立てだけ)wireAuth.ts(認証関連だけ) 最後にbootstrap()で合体🤝
(2) Factoryで読みやすくする🏭
// src/entry/factories/createTodoUsecases.ts
import { AddTodo } from "../../app/addTodo";
import { TodoRepo, Clock, Logger } from "../../domain/ports";
export function createTodoUsecases(deps: { repo: TodoRepo; clock: Clock; logger: Logger }) {
return {
addTodo: new AddTodo(deps.repo, deps.clock, deps.logger),
};
}
bootstrap() はこうなる💎
import { createTodoUsecases } from "./factories/createTodoUsecases";
// ...repo/clock/logger を作る...
const todo = createTodoUsecases({ repo, clock, logger });
return { ...todo };
8) import地雷の避け方💣🧯(TSなら特に大事!)
✅ ルール3つだけ🧷
-
環境依存の import は entry に寄せる 例:
node:fsは Node用entryに、windowは Web用entryに🪟 -
barrel(index.tsでまとめexport)を乱用しない📦⚠️ 便利だけど循環importの温床になりやすい😵
-
tsconfigのモジュール解決は、プロジェクト種別に合わせる
moduleResolutionは挙動が変わる大事ポイントだよ(公式リファレンスあり)📚 (TypeScript)
9) テストでは Composition Root をどうする?🧪💖
✅ 基本:ユニットテストは“自分で組み立てる”
import { AddTodo } from "../src/app/addTodo";
import { MemoryTodoRepo } from "../src/infra/memoryTodoRepo";
test("AddTodo adds an item", async () => {
const repo = new MemoryTodoRepo();
const clock = { now: () => new Date("2026-01-16T00:00:00Z") };
const logger = { info: (_: string) => {} };
const addTodo = new AddTodo(repo, clock, logger);
const todo = await addTodo.execute("テスト書く🧪✨");
expect(repo.items[0].id).toBe(todo.id);
});
テストは “その場で必要な依存を作って注入” がいちばん分かりやすいよ😊 (本番のComposition Rootに引っ張られないのがコツ!)
10) ミニ課題(やってみよ〜!)🎀✍️
課題A:new を探して1か所に集める🔎🧹
newを検索して、entry/bootstrap に寄せられるものを2つ移動してみよう✨
課題B:環境依存importを隔離する🪟🧊
windowを触ってる場所が app/domain にあったら、infraに移して、entryで注入!
課題C:組み立て図を描く📦➡️📦
UI → UseCase → Ports → Infraの矢印を紙に描く📝 (絵が下手でもOK!矢印が勝ち🏆)
11) AI活用コーナー🤖✨(超おすすめプロンプト)
- 「このプロジェクトの
newを Composition Root に集めたい。移動候補を列挙して、移動後のファイル案も出して」 - 「循環importが起きそうな箇所を疑って、怪しい依存関係を説明して」
- 「entry/bootstrap が肥大化してきた。wireごとの分割案を3パターン出して」
まとめ🏁💖
- Composition Rootは **“組み立て専用の場所”**🏭
newと 環境依存import をそこに集めると、変更・テスト・安全性が一気に良くなる🎁- 次の章(Factory)で、Composition Root を“太らせずに保つ”テクをもっと磨いていこうね🏃♀️💨✨