第20章 import地獄対策①:境界ルールを1つ決める📦🚫
この章のゴールはシンプルだよ💡 「import(依存)の向き」の“たった1個のルール”を決めて、守れる仕組みまで作ること😊🧩
ちなみに本日時点だと TypeScript の最新は 5.9系(5.9.3) だよ🆕✨(公式DLページでも “currently 5.9” 表記) (TypeScript) TS 5.9 では Node向けの安定オプション(
node20)やimport deferみたいな “モジュール周り” にも動きがあるので、import設計の重要度がさらに上がってる感じ🧠⚙️ (TypeScript)
1) 「import地獄」ってどんな状態?😵💫🌀
よくある症状はこれ👇
- どこからでもどこへでも import できちゃう(無法地帯)🏜️
- Domainっぽいところが fetch / DB / localStorage を直叩きしはじめる🌐🗄️
- ちょっと直すと、別ファイルが雪崩れる☃️💥
- テストが「準備だらけ」になって、やる気が消える🧪😇
- 循環 import が出て、謎のバグ&謎の初期化順になる🌀👻
ここで大事なのは、解決策が 「頑張って気をつける」じゃなくて 「ルール+自動チェック(lint)」 になってることだよ🛡️✨
2) まずは境界ルールを“1個だけ”決めよう🎯📏

おすすめの最初の1個はこれ👇(超王道で強い💪)
✅ ルール:domain は infra を import しない🚫📦
- domain:純粋ロジック中心(ルール・計算・判断)🧠✨
- infra:外部I/O(HTTP/DB/Storage/OS/Envなど)🌐🗄️⚙️
- だから domain は infra を知らない(知らなくていい)🙈✨
これを決めるだけで、DI/IoCが一気にキレイになるよ💉🧩 (「差し替え」できる形が自然にできる)
3) 具体例:フォルダ境界を作る🗂️✨
例として、こう分ける👇(名前は好みでOKだけど、まず固定しよ😊)
domain/(中心)🧠infra/(外側のI/O)🌐app/(ユースケース組み立て)🧩entry/(起動、Composition Root)🚀
イメージはこんな感じ👇
entry ─────────────▶ infra
│ ▲
│ │ implements
▼ │
app ───────────────▶ domain
uses ▲
│ defines "ports" (interfaces/types)
ポイントはこれ💡 domain が「こういう機能が欲しい」を“型(契約)”として定義して、 infra がそれを実装する(逆転!)🔁✨
4) 小さなサンプル(DIが自然にキレイになる)💉🌸
domain(欲しい能力=ポートを定義)📜
// domain/ports/Clock.ts
export interface Clock {
now(): Date;
}
// domain/usecases/isExpired.ts
import type { Clock } from "../ports/Clock";
export function makeIsExpired(clock: Clock) {
return (expiresAt: Date) => clock.now().getTime() > expiresAt.getTime();
}
infra(外側が実装する)🧰
// infra/SystemClock.ts
import type { Clock } from "../domain/ports/Clock";
export class SystemClock implements Clock {
now(): Date {
return new Date();
}
}
entry(組み立て:Composition Root)📍🚀
// entry/main.ts
import { SystemClock } from "../infra/SystemClock";
import { makeIsExpired } from "../domain/usecases/isExpired";
const clock = new SystemClock();
const isExpired = makeIsExpired(clock);
console.log(isExpired(new Date(Date.now() - 1000)));
🌟この形のいいところ
- domain は infra を一切知らない🙌
- 差し替えが簡単(FakeClockでテストし放題)🧪💖
- 「どこで組み立てるか」が明確(entryだけ)📍✨
5) ルールを“守らせる”仕組み(ESLint)🛡️✅
5-1. いちばん簡単:no-restricted-imports(ESLint標準)🚫📥
ESLintには「この import 禁止!」を指定できるルールがあるよ✅ (ESLint) そして ESLint v9 では flat config(eslint.config.js)が標準になってる🧩 (ESLint)
例:domain 配下から infra を import したらアウトにする👇
// eslint.config.js
export default [
{
files: ["src/domain/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["../infra/*", "../../infra/*", "**/infra/**"],
message: "domain から infra を import しないでね🙅♀️(境界ルール)",
},
],
},
],
},
},
];
パスはプロジェクトの構成で変わるから、最初は “ゆるめに patterns” でもOKだよ😊✨ 「まず守れる」を優先しよ〜🌸
5-2. もう少し強く:import/no-restricted-paths(eslint-plugin-import)🧱✨
「どのフォルダから、どのフォルダを禁止」みたいに ゾーン指定できるやつだよ📦🚧 (GitHub)
(例の考え方:target側(domain)では from側(infra)を禁止)
5-3. さらに本格派:Boundaries(eslint-plugin-boundaries / JS Boundaries)🧠🏗️
“アーキテクチャ境界”をルール化して守らせる系✨
中心ルール boundaries/element-types で、要素タイプの行き来を制御できるよ🧩 (jsboundaries.dev)
5-4. モノレポなら:Nx の “module boundaries” も強い💪📚
Nx には境界を強制するルール(@nx/enforce-module-boundaries)があるよ🧱 (Nx)
6) ありがちな違反パターンと直し方🔧😺
❌ domain が infra を直接呼ぶ
// domain/... にこんなのがある
import { fetchJson } from "../infra/http/fetchJson";
✅ 直し方(ポートを作る)
- domain:
HttpClientみたいな interface(契約) を定義📜 - infra:
FetchHttpClientが実装🌐 - entry:注入して組み立て📍
「欲しい形は domain が言う」「実現は infra がやる」🧠➡️🧰✨
7) ミニ課題(手を動かす)📝💪✨
課題A:ルール1個を“文章にする”📜
- 「domain は infra を import しない」
- これを 1行で README に書く✍️✨
課題B:違反を1個だけ直す✂️
src/domainからinfraを import してる箇所を1つ見つける🔎- “ポート化”して entry で組み立てに移動📍
課題C:lintで強制する🛡️
- ESLintにルール追加
- わざと違反importを書いて、ちゃんと怒られるのを確認😈✅
8) AI(Copilot/Codex)に頼むと爆速になるお願いテンプレ🤖✨
そのままコピペで使えるよ🫶
- 「このファイルの import が境界ルール(domain→infra禁止)に違反してないか確認して、違反があれば最小の修正案を出して」🔍🧩
- 「domain に必要な ‘ポート(interface)’ を提案して。命名もお願い」📜✨
- 「entry(Composition Root)に寄せる形で、依存の組み立てコードを書き直して」📍🚀
- 「ESLint flat config で domain→infra import を禁止する設定例を出して」🛡️
まとめ🎀🏁
- import地獄の特効薬は “境界ルールを1つ決める” こと🎯✨
- まずは domain は infra を import しない から始めるのが強い💪📦
- ルールは ESLintで自動強制 して、気合いに頼らない🛡️😊 (ESLint)
- これができると、DI/IoCが「自然に」キレイになる💉🧩💖
次の章(第21章)では、この境界ルールが守りやすくなるように フォルダを3層で整理して、迷子ゼロ構成にしていくよ🗂️🌸