第4章:TypeScriptで合成するための道具箱① interface / type 📘✨
(テーマ:「差し替え可能」にするための“約束”を作れるようになる🧩🔁)
0) 今日のゴール🎯💖
この章が終わったら、こんなことができるようになります👇✨
- 「部品」を入れ替えられる形(差し替え可能)で設計できる🔁🧩
interfaceとtypeを「迷いにくい基準」で使い分けできる🧠✨- “同じ形ならOK”な TypeScript の性質(構造的型付け)を味方にできる🦆✅
Loggerを例に、本番用とテスト用をスイッと切り替えられる💡🧪
※ちなみに2026年1月時点だと、TypeScript の安定版は 5.9 系が配布されています📦✨(GitHub Releases) (GitHub)
1) なんで interface / type が合成で超大事なの?🤔🧩
合成って「部品を組み合わせる」設計なんだけど、そこで一番困るのがコレ👇
部品を差し替えたいのに、呼び出し側が“特定の実装”にベタ依存してる😵💫💥
たとえば、サービスの中でいきなり new ConsoleLogger() しちゃうと…
- テストでログを「メモリにためる版」に変えたいのに変えられない😢
- 本番でログの出し方を変えたいのに、コード改造が必要になる😱
そこで登場するのが 「約束(契約)」 📜✨ 呼び出し側は「この約束を守ってる部品なら誰でもOK!」って考えると、差し替えが一気に楽になります🔁💖
2) まずは結論:interface と type の“迷わない”使い分け🧠✨
最初はこのルールでOKです👇(慣れたら例外も使う!)
- オブジェクトの形(プロパティ/メソッドの集合)を表したい →
interfaceが気持ちいい🧩 - 合体(&)/どれか(|)/関数型/ユニオン系 →
typeが強い💪✨
ざっくり表にすると👇
-
interface:- ✅ “部品の接続口”を作るのに向く(差し替え設計がしやすい)
- ✅
extendsで拡張しやすい - ✅ 同名宣言の“マージ”がある(便利だけど、初心者は知らなくてもOK)
-
type:- ✅
A | B(どっちか)やA & B(合体)が得意 - ✅ 関数型
type Fn = (x: number) => numberが自然 - ✅ Utility Types(
Omit/Pick/Partialなど)と相性よい
- ✅
3) いちばん大事:TypeScript は「形が同じならOK」🦆✅(構造的型付け)
TypeScript は、クラス名や継承関係よりも “中身の形” を見ます👀✨ つまり…
Loggerという名前の型を実装してなくても- 同じメソッドを持ってれば
Loggerとして扱えることが多いです🧩💖
この性質が、合成(部品の差し替え)と相性バツグン!🔁✨
4) 「差し替え可能」にする最低条件🔁📜
差し替えできるってことはつまり…
✅ 同じメソッド名 ✅ 同じ引数 ✅ 同じ戻り値
この3点が揃ってることです🎀✨ これが “約束(契約)” だよ〜ってイメージです📘💕
5) ミニ題材:Logger を差し替えできるようにする📝🔁
ここからは「合成のための型の作り方」を、めっちゃ手を動かして理解します✋✨
5-1) interface で “接続口” を作る🧩🔌

export interface Logger {
info(message: string): void;
warn(message: string): void;
error(message: string, err?: unknown): void;
}
ポイント💡
errorのerrはunknownがおすすめ✨(なんでも来る世界に強い🛡️)- 戻り値は
void(ログは基本「何かを返す」より「記録する」だよね📝)
5-2) 本番用:ConsoleLogger(普通に出す)🖥️✨
import { Logger } from "./Logger";
export class ConsoleLogger implements Logger {
info(message: string): void {
console.log(`INFO: ${message}`);
}
warn(message: string): void {
console.warn(`WARN: ${message}`);
}
error(message: string, err?: unknown): void {
console.error(`ERROR: ${message}`, err);
}
}
5-3) テスト用:MemoryLogger(メモリにためる)🧪🫙
import { Logger } from "./Logger";
type LogLevel = "info" | "warn" | "error";
export type LogEntry = {
level: LogLevel;
message: string;
err?: unknown;
at: Date;
};
export class MemoryLogger implements Logger {
private readonly entries: LogEntry[] = [];
info(message: string): void {
this.entries.push({ level: "info", message, at: new Date() });
}
warn(message: string): void {
this.entries.push({ level: "warn", message, at: new Date() });
}
error(message: string, err?: unknown): void {
this.entries.push({ level: "error", message, err, at: new Date() });
}
// テストで中身を確認できるようにする💖
getEntries(): readonly LogEntry[] {
return this.entries;
}
}
ここで type も活躍してるよね✨
LogLevel = "info" | "warn" | "error"← ユニオンはtypeが得意🎛️LogEntryみたいな「データの形」もtypeでOK👌💕
6) 合成っぽくする:呼び出し側は Logger だけ知ってればOK🧩🔁
import { Logger } from "./Logger";
export class UserService {
constructor(private readonly logger: Logger) {}
registerUser(name: string): void {
if (name.trim().length === 0) {
this.logger.warn("名前が空っぽだよ〜😢");
return;
}
// 何か登録処理がある想定✨
this.logger.info(`ユーザー登録したよ🎉 name=${name}`);
}
}
ここが合成の気持ちよさ😍✨
UserServiceはConsoleLoggerを知らないLoggerという“約束”だけ見てる- だから差し替えが簡単🔁💖
7) 使い分けデモ:本番とテストで差し替え🪄🔁
本番っぽい使い方🖥️
import { UserService } from "./UserService";
import { ConsoleLogger } from "./ConsoleLogger";
const service = new UserService(new ConsoleLogger());
service.registerUser("こみやんま");
テストっぽい使い方🧪
import { UserService } from "./UserService";
import { MemoryLogger } from "./MemoryLogger";
const logger = new MemoryLogger();
const service = new UserService(logger);
service.registerUser(""); // わざと失敗させる
console.log(logger.getEntries());
// → warn が入ってるはず!🎯
8) type が輝く場面:合体(&)と “どれか”(|)🧠✨
8-1) 「基本ログ + 追跡ID」みたいに合体したい🧩➕
type WithTrace = { traceId: string };
type TracedLogEntry = LogEntry & WithTrace;
8-2) 「成功 or 失敗」のどっちかを表したい🎲
type Ok<T> = { ok: true; value: T };
type Ng = { ok: false; reason: string };
type Result<T> = Ok<T> | Ng;
こういうのは type がめっちゃ読みやすいです💖
9) “約束”設計のコツ:interface は小さく、鋭く🔪✨
合成をしやすくするための、超ありがちな黄金ルール🌟
✅ ルール1:メソッドを増やしすぎない
Logger に「ファイル保存」「DB保存」「Slack通知」…とか入れ始めると、
差し替えたいのに 全部実装しなきゃ になって地獄です👻💥
👉 解決:用途ごとに分ける✨
Logger(ログ)Notifier(通知)AuditTrail(監査) みたいにね🧩💕
✅ ルール2:戻り値・例外の方針をそろえる
- 「失敗したら throw?」
- 「Result で返す?」
- 「void で黙る?」
ここがバラバラだと差し替えできなくなる😱
最初はシンプルに、ログは void でOK👌✨
✅ ルール3:引数は “狭い型” より “受け止める型” を意識
err?: unknown みたいにしておくと、現実の混沌に強い🌀🛡️
10) AI拡張に頼むときのコツ🤖✨(第4章バージョン)
この章の範囲で、AIに頼むならこんな感じが強いです💖
- 「このクラスが
newで依存を作ってる。interfaceを作って差し替え可能にして」🧩🔁 - 「
typeを使ってResult<T>を作って、成功/失敗をユニオンで表して」🎲✨ - 「
Loggerのテスト用実装(MemoryLogger)を作って」🧪🫙
出てきたコードは、最後にここだけチェック✅
interfaceがデカすぎない?(神インターフェース化してない?)😇- 依存が “具体クラス” じゃなく “約束” になってる?🔁
- テスト用が作りやすい形?🧪✨
11) ミニ演習✍️💖(手を動かすゾ!)
演習A:Logger に debug を足してみよう🐛✨
Loggerにdebug(message: string): voidを追加ConsoleLoggerとMemoryLoggerも追従UserServiceで、成功時にdebugも呼ぶ
✅できたら:MemoryLogger の entries に debug が入るように拡張してみよ〜🫶
演習B:「約束を小さくする」練習✂️🧩
Logger をこう分けてみて✨
InfoLogger:infoだけErrorLogger:errorだけ
そして UserService は「必要な方だけ」受け取るようにしてみよう🎯
👉 “必要な約束だけ依存する” の感覚が育つよ🌱💖
12) 章末ミニテスト📝✨(答えは下にあるよ👇)
-
合成で
interfaceが大事な理由は? A. 継承を強くするため B. 部品を差し替え可能にするため C. クラス数を増やすため -
typeが特に得意なのは? A. 同名マージ B. ユニオン(A | B) C. クラス継承 -
TypeScript が型を判定するとき、強く見るのは? A. クラス名 B. 継承関係 C. 形(構造)
-
err: anyよりerr: unknownが嬉しい理由は? A. なんでも安全に使えるから B. 使う前に確認を促せるから C. 速くなるから -
“差し替え可能”の最低条件は? A. メソッド名だけ同じ B. 引数だけ同じ C. メソッド名・引数・戻り値が同じ
答え🎀
- B / 2) B / 3) C / 4) B / 5) C ✅🎉
13) まとめ🎁✨
- 合成で強いのは「部品の差し替え」🔁🧩
- 差し替えの鍵は「約束(契約)」=
interface/type📜✨ interfaceは“接続口”にピッタリ、typeはユニオンや合体に強い💪💕- TypeScript は“形が同じならOK”だから、合成と相性最高🦆✅
次の第5章では、この「約束で差し替え可能にする」をさらに進めて、依存を外から渡す(newしない) をやっていきます🚚💨✨