メインコンテンツまでスキップ

第6章 TypeScriptの武器:type/interfaceで“契約”を作る📜🧡

この章はね、「中心(domain/app)が、外側(DB/HTTP/SDK)を直接知らなくても動ける」状態を作るための超大事な準備だよ〜!😊✨ そのカギが “契約(contract)”= interface / type なの🧩🔑

ちなみに2026年1月時点だと TypeScript の最新系は **5.9.x(npm上は 5.9.3)**だよ。(npmjs.com) (でもこの章でやる “契約” の考え方は、バージョンが進んでもずっと使えるやつ!💪)


6-0. 今日のゴール🎯✨

  • 契約 =「欲しい能力だけ」を言葉にするって体感する
  • ✅ Domain側は 契約だけ import(= 実装を見ない)にする
  • interfacetype を、迷わず使い分けできるようになる
  • ✅ テストで差し替えしやすい “美しい依存” を作れる🧪💞

6-1. “契約”ってなに?📜 ざっくり言うと…

**契約(contract)**は、

「私はこの機能が欲しい!でも“どうやって”は知らない!」

っていう宣言だよ😌🧡

たとえば Domain が欲しいのは「ユーザーを保存できること」であって、 「Prismaで〜」とか「SQLで〜」とかは外側の都合だよね?🌀

だから Domain はこう言うの👇

  • UserRepository(ユーザーを保存/取得できる)
  • Clock(今の時刻がわかる)
  • PaymentGateway(課金できる)

こういう “欲しい能力” を interface/type で定義しておくと、 Domain は外側に支配されない💪✨


6-2. TypeScriptの型は「形(shape)」を見る👀🧩

TypeScript は「この値はどんな形?」っていう **構造(shape)**で型チェックするよ。(TypeScript) だから、契約(interface/type)は **“形の約束”**としてすごく相性がいいの😊✨


6-3. interfacetype、どっち使うの?😵‍💫→こう考えると楽!

結論:両方使う! ただし役割で分けると迷いが減るよ🧠✨

使い分けの目安🧭

  • interface

    • 「サービスっぽい契約」= 〜できる(Repository / Gateway / Clock)に向いてる
    • 後から拡張しやすい(再オープンして追加できる)(TypeScript)
  • type

    • 「データの形」や「合体/分岐(union/intersection)」に強い
    • Result みたいな 成功/失敗の表現が作りやすい💡

ざっくり: “振る舞いの契約”は interface、 “データの表現”は type


6-4. 契約の命名のコツ📛✨(ここ超大事!)

契約の名前は、「実装の匂い」を消すのがコツだよ🧼💕

✅ 良い命名(契約っぽい)

  • UserRepository
  • PaymentGateway
  • Clock
  • EmailSender
  • Logger

❌ うーん…(実装が漏れてる)

  • PrismaUserRepository
  • StripePaymentGateway
  • FetchEmailSender
  • ConsoleLogger

実装名は Adapters側に閉じ込めるのが正解〜!🚪✨


6-5. まずは小さい契約を3つ作ってみよ🧪💕

この章のミニ題材は「ユーザー登録」っぽいやつにするね😊 Domain が欲しい能力はこの3つ👇

  • UserRepository(保存/存在チェック)
  • Clock(今の時刻)
  • IdGenerator(ID発行)

① 契約を作る(domain側)📜

// src/domain/contracts/UserRepository.ts
export interface UserRepository {
existsByEmail(email: string): Promise<boolean>;
save(user: User): Promise<void>;
}

// src/domain/contracts/Clock.ts
export interface Clock {
now(): Date;
}

// src/domain/contracts/IdGenerator.ts
export interface IdGenerator {
newId(): string;
}

// src/domain/User.ts
export type User = {
id: string;
email: string;
createdAt: Date;
};

ポイントはこれ👇✨

  • Domain は “できること”だけ言う
  • 具体的なDBもライブラリも一切出てこない😌🧡

6-6. Domainのユースケースは「契約だけ」見て書く✍️🧅

// src/domain/registerUser.ts
import type { UserRepository } from "./contracts/UserRepository";
import type { Clock } from "./contracts/Clock";
import type { IdGenerator } from "./contracts/IdGenerator";
import type { User } from "./User";

export type RegisterUserDeps = {
users: UserRepository;
clock: Clock;
ids: IdGenerator;
};

export type RegisterUserInput = { email: string };

export async function registerUser(
deps: RegisterUserDeps,
input: RegisterUserInput
): Promise<User> {
const email = input.email.trim().toLowerCase();

if (await deps.users.existsByEmail(email)) {
throw new Error("Email already exists"); // ここは第11章で綺麗にするよ🧯✨
}

const user: User = {
id: deps.ids.newId(),
email,
createdAt: deps.clock.now(),
};

await deps.users.save(user);
return user;
}

ここが最高にえらいポイント👑✨

  • Domain は 契約(interface)しか import してない
  • DBもHTTPも知らない
  • なのにちゃんと動く(外側が差し込めばOK)🔄💕

6-7. “satisfies” で「契約を満たしてる?」を安全にチェック✅🪄

契約を満たしてるかチェックしつつ、型推論はそのままにできるのが satisfies!(TypeScript) モックやインメモリ実装でめっちゃ便利だよ〜😊

// src/adapters/inMemoryUserRepository.ts
import type { UserRepository } from "../domain/contracts/UserRepository";
import type { User } from "../domain/User";

const store = new Map<string, User>();

export const inMemoryUserRepository = {
async existsByEmail(email: string) {
for (const u of store.values()) {
if (u.email === email) return true;
}
return false;
},
async save(user: User) {
store.set(user.id, user);
},
} satisfies UserRepository;

as UserRepository って書いちゃうと「嘘でも通る」ことがあるけど、 satisfies満たしてないとちゃんと怒ってくれるから安心〜!🧯✨


6-8. 契約の“粒度”ってどう決めるの?🍰📏

初心者のうちは、これだけ守ると失敗しにくいよ😊

粒度のルール(まずはこれだけ)✅

  • ✅ 1契約 = 1責務(Repository / Clock / Payment みたいに役割で分ける)
  • ✅ メソッド名は「何をしたいか」が伝わる動詞にする(save, find, charge
  • ✅ 引数/戻り値は Domainの言葉で(DTOやHTTPレスポンス型を入れない)
  • ✅ 迷ったら「この契約、テストで差し替えたい?」で判断🧪✨

6-9. “データ”の契約は type が気持ちいい🫧✨(Result型ちょい入門)

エラーを throw で投げるの、後で第11章で綺麗にするんだけど… 準備として「成功/失敗」を type で表すとこうなるよ👇

// src/domain/Result.ts
export type Ok<T> = { ok: true; value: T };
export type Err<E> = { ok: false; error: E };
export type Result<T, E> = Ok<T> | Err<E>;

export const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
export const err = <E>(error: E): Err<E> => ({ ok: false, error });

type は union が得意だから、こういう表現がスルッと書ける🪄✨


6-10. ミニ演習👩‍💻✨(手を動かすやつ!)

演習A:契約を3つ作ろう📜

次の契約を src/domain/contracts/ に作ってね👇

  • PaymentGatewaycharge(amountYen: number): Promise<void>
  • EmailSendersend(to: string, subject: string, body: string): Promise<void>
  • Loggerinfo(message: string): void / error(message: string): void

💡コツ:実装名(Stripe/SendGrid/Console)を絶対入れない!


演習B:ユースケースを「契約だけ」で書こう🧅

purchase.ts を作って、PaymentGatewayEmailSender を deps に持たせてね😊

  • 課金する
  • 完了メール送る

演習C:テスト用の差し替えを satisfies で作ろう✅🪄

fakePaymentGateway を作って、呼ばれた回数を数えるとかしてみて! (“ちゃんと呼ばれた?”をテストしやすくなるよ🧪✨)


6-11. AI活用プロンプト集🤖💌(コピペOK)

① 契約の候補名を出してもらう🧠✨

  • 「ユーザー登録で必要な外部依存を 契約名だけ で10個提案して。実装技術名(Prisma/Stripe等)は禁止。」

② 契約の粒度レビューしてもらう📏👀

  • 「この interface は責務が広すぎる?分割案を3パターン出して。理由も添えて。」

③ “実装が漏れてる命名”を直してもらう🧼

  • PrismaUserRepository みたいな名前になっちゃった。契約として自然な名前に直して、なぜその名前が良いか説明して。」

satisfies で安全なモックを作ってもらう🪄✅

  • 「この interface を満たす in-memory 実装を satisfies を使って作って。テストで呼び出し回数も追跡したい。」

6-12. まとめ🌸✨(この章で得た最強スキル)

  • 🎯 契約(interface/type)は “中心を守る盾”
  • 🧅 Domain は 契約だけを見る(実装を知らない)
  • interface=振る舞いの契約、type=データ表現(まずはこの目安でOK)(TypeScript)
  • 🪄 satisfies はモック/差し替えで超便利!(TypeScript)
  • 👀 TypeScript は “形で見る型” だから、契約設計と相性バツグン!(TypeScript)

次の第7章で、いよいよこの契約を使って 「中心→外側 import を断つ」(DIP体験)に突入できるよ〜!🔄🚫💪✨