【2026年版】TypeScriptデザインパターン実践ガイド:型安全で堅牢なコードの書き方

Tech Trends AI
- 9 minutes read - 1814 wordsはじめに
TypeScriptは2026年現在、フロントエンドからバックエンド、さらにはAIアプリケーション開発に至るまで、幅広い領域で事実上の標準言語となっています。しかし、TypeScriptの真価は単に「JavaScriptに型が付く」ことではなく、型システムを設計の道具として活用することで、コンパイル時にバグを防ぎ、保守性の高いコードを書ける点にあります。
本記事では、GoF(Gang of Four)の古典的パターンをTypeScriptで型安全に再実装する方法から、2026年のモダン開発で求められる関数型パターン、DI(依存性注入)パターン、イベント駆動パターンまで、実践的なコード例とともに解説します。
なぜTypeScriptでデザインパターンを学ぶべきか
型システムが設計を強制する
JavaScriptでは設計パターンを「規約」として守る必要がありますが、TypeScriptではインターフェースやジェネリクスを活用し、パターンの構造をコンパイラが検証してくれます。
| 観点 | JavaScript | TypeScript |
|---|---|---|
| インターフェース準拠 | 実行時エラーで発覚 | コンパイル時に検出 |
| パターンの構造保証 | ドキュメント依存 | 型定義で強制 |
| リファクタリング安全性 | テスト依存 | 型エラーで検出 |
| IDE支援 | 限定的 | 完全な補完・ナビゲーション |
| チーム開発 | コードレビュー依存 | 型が仕様書になる |
2026年のTypeScript開発トレンド
2026年のTypeScript開発では、以下のトレンドがデザインパターンの選択に影響しています。
- Effect-TS / fp-ts の普及: 関数型プログラミングパターンの主流化
- Branded Types の活用: 原始型の意味的な区別
- Template Literal Types: 文字列ベースAPIの型安全化
- Satisfies演算子の定着: 型推論とバリデーションの両立
- Decorator(Stage 3)の安定化: メタプログラミングパターンの標準化
生成パターン(Creational Patterns)
Factory Method パターン
Factory Methodパターンは、オブジェクト生成のインターフェースを定義し、実際のインスタンス化をサブクラスに委ねるパターンです。TypeScriptではジェネリクスと組み合わせることで、強力な型推論が得られます。
// プロダクトのインターフェース
interface Notification {
readonly type: string;
send(message: string): Promise<void>;
}
// 具体的なプロダクト
class EmailNotification implements Notification {
readonly type = "email" as const;
constructor(private readonly address: string) {}
async send(message: string): Promise<void> {
console.log(`Email to ${this.address}: ${message}`);
}
}
class SlackNotification implements Notification {
readonly type = "slack" as const;
constructor(private readonly channel: string) {}
async send(message: string): Promise<void> {
console.log(`Slack #${this.channel}: ${message}`);
}
}
// Factory(型安全なマップベース)
const notificationFactories = {
email: (config: { address: string }) => new EmailNotification(config.address),
slack: (config: { channel: string }) => new SlackNotification(config.channel),
} as const;
type NotificationType = keyof typeof notificationFactories;
// 型安全なファクトリ関数
function createNotification<T extends NotificationType>(
type: T,
config: Parameters<(typeof notificationFactories)[T]>[0]
): ReturnType<(typeof notificationFactories)[T]> {
return notificationFactories[type](config as any) as any;
}
// 使用例:型が完全に推論される
const email = createNotification("email", { address: "user@example.com" });
const slack = createNotification("slack", { channel: "general" });
Builder パターン
Builderパターンは、複雑なオブジェクトの構築過程を分離します。TypeScriptではメソッドチェーンの型追跡により、必須フィールドの設定漏れをコンパイル時に検出できます。
// 状態を型パラメータで追跡するBuilder
interface QueryBuilderState {
hasTable: boolean;
hasSelect: boolean;
}
class QueryBuilder<State extends QueryBuilderState = { hasTable: false; hasSelect: false }> {
private table?: string;
private columns: string[] = [];
private conditions: string[] = [];
private orderByClause?: string;
private limitValue?: number;
from(table: string): QueryBuilder<State & { hasTable: true }> {
this.table = table;
return this as any;
}
select(...columns: string[]): QueryBuilder<State & { hasSelect: true }> {
this.columns = columns;
return this as any;
}
where(condition: string): this {
this.conditions.push(condition);
return this;
}
orderBy(column: string, direction: "ASC" | "DESC" = "ASC"): this {
this.orderByClause = `${column} ${direction}`;
return this;
}
limit(value: number): this {
this.limitValue = value;
return this;
}
// buildはtableとselectが設定済みの場合のみ呼び出せる
build(
this: QueryBuilder<{ hasTable: true; hasSelect: true }>
): string {
let query = `SELECT ${this.columns.join(", ")} FROM ${this.table}`;
if (this.conditions.length > 0) {
query += ` WHERE ${this.conditions.join(" AND ")}`;
}
if (this.orderByClause) {
query += ` ORDER BY ${this.orderByClause}`;
}
if (this.limitValue) {
query += ` LIMIT ${this.limitValue}`;
}
return query;
}
}
// 使用例
const query = new QueryBuilder()
.from("users")
.select("id", "name", "email")
.where("active = true")
.orderBy("created_at", "DESC")
.limit(10)
.build(); // OK: fromとselectが設定済み
// const invalid = new QueryBuilder()
// .from("users")
// .build(); // コンパイルエラー!selectが未設定
Singleton パターン(モジュールスコープ)
TypeScriptでは、クラスベースのSingletonよりもモジュールスコープを活用する方がシンプルです。
// config.ts - モジュール自体がシングルトンとして機能
interface AppConfig {
readonly apiBaseUrl: string;
readonly maxRetries: number;
readonly timeout: number;
}
const config: AppConfig = Object.freeze({
apiBaseUrl: process.env.API_BASE_URL ?? "https://api.example.com",
maxRetries: Number(process.env.MAX_RETRIES ?? 3),
timeout: Number(process.env.TIMEOUT ?? 5000),
});
export default config;
構造パターン(Structural Patterns)
Adapter パターン
Adapterパターンは、互換性のないインターフェース同士を接続します。外部APIの統合で頻繁に使用されます。
// 統一インターフェース
interface PaymentGateway {
charge(amount: number, currency: string): Promise<PaymentResult>;
refund(transactionId: string, amount: number): Promise<RefundResult>;
}
interface PaymentResult {
success: boolean;
transactionId: string;
timestamp: Date;
}
interface RefundResult {
success: boolean;
refundId: string;
}
// Stripe SDK のアダプタ
class StripeAdapter implements PaymentGateway {
constructor(private readonly stripeClient: StripeClient) {}
async charge(amount: number, currency: string): Promise<PaymentResult> {
const intent = await this.stripeClient.paymentIntents.create({
amount: Math.round(amount * 100), // Stripeはセント単位
currency: currency.toLowerCase(),
});
return {
success: intent.status === "succeeded",
transactionId: intent.id,
timestamp: new Date(),
};
}
async refund(transactionId: string, amount: number): Promise<RefundResult> {
const refund = await this.stripeClient.refunds.create({
payment_intent: transactionId,
amount: Math.round(amount * 100),
});
return {
success: refund.status === "succeeded",
refundId: refund.id,
};
}
}
// PayPal SDK のアダプタ
class PayPalAdapter implements PaymentGateway {
constructor(private readonly paypalClient: PayPalClient) {}
async charge(amount: number, currency: string): Promise<PaymentResult> {
const order = await this.paypalClient.orders.create({
purchase_units: [{ amount: { value: amount.toString(), currency_code: currency } }],
});
return {
success: order.status === "COMPLETED",
transactionId: order.id,
timestamp: new Date(),
};
}
async refund(transactionId: string, amount: number): Promise<RefundResult> {
const result = await this.paypalClient.payments.refund(transactionId, {
amount: { value: amount.toString() },
});
return {
success: result.status === "COMPLETED",
refundId: result.id,
};
}
}
Decorator パターン(TC39 Decorators)
TypeScript 5.x以降で安定したTC39 Decoratorsを活用するパターンです。
// ログデコレータ
function logged<T extends (...args: any[]) => any>(
originalMethod: T,
context: ClassMethodDecoratorContext
): T {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`[${methodName}] called with:`, args);
const start = performance.now();
const result = originalMethod.call(this, ...args);
if (result instanceof Promise) {
return result.then((val: any) => {
console.log(`[${methodName}] resolved in ${performance.now() - start}ms`);
return val;
});
}
console.log(`[${methodName}] returned in ${performance.now() - start}ms`);
return result;
}
return replacementMethod as T;
}
// リトライデコレータ
function retry(maxAttempts: number = 3, delayMs: number = 1000) {
return function <T extends (...args: any[]) => Promise<any>>(
originalMethod: T,
_context: ClassMethodDecoratorContext
): T {
async function replacementMethod(this: any, ...args: any[]) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await originalMethod.call(this, ...args);
} catch (error) {
if (attempt === maxAttempts) throw error;
await new Promise((r) => setTimeout(r, delayMs * attempt));
}
}
}
return replacementMethod as T;
};
}
// 使用例
class ApiClient {
@logged
@retry(3, 500)
async fetchData(endpoint: string): Promise<unknown> {
const res = await fetch(endpoint);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
}
振る舞いパターン(Behavioral Patterns)
Strategy パターン
Strategyパターンは、アルゴリズムを切り替え可能にします。TypeScriptではDiscriminated Unionと組み合わせると強力です。
// 戦略の型定義
type CompressionStrategy =
| { type: "gzip"; level: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 }
| { type: "brotli"; quality: number }
| { type: "none" };
// 各戦略の実装(関数型アプローチ)
const compressionHandlers: {
[K in CompressionStrategy["type"]]: (
data: Buffer,
config: Extract<CompressionStrategy, { type: K }>
) => Promise<Buffer>;
} = {
gzip: async (data, config) => {
const zlib = await import("zlib");
return new Promise((resolve, reject) =>
zlib.gzip(data, { level: config.level }, (err, result) =>
err ? reject(err) : resolve(result)
)
);
},
brotli: async (data, config) => {
const zlib = await import("zlib");
return new Promise((resolve, reject) =>
zlib.brotliCompress(
data,
{ params: { [zlib.constants.BROTLI_PARAM_QUALITY]: config.quality } },
(err, result) => (err ? reject(err) : resolve(result))
)
);
},
none: async (data) => data,
};
// 型安全なディスパッチ
async function compress(data: Buffer, strategy: CompressionStrategy): Promise<Buffer> {
const handler = compressionHandlers[strategy.type];
return handler(data, strategy as any);
}
Observer パターン(型安全EventEmitter)
型安全なイベントシステムを構築します。
// イベントマップの型定義
interface AppEvents {
"user:login": { userId: string; timestamp: Date };
"user:logout": { userId: string };
"order:created": { orderId: string; total: number };
"order:cancelled": { orderId: string; reason: string };
}
// 型安全なEventEmitter
class TypedEventEmitter<Events extends Record<string, unknown>> {
private listeners = new Map<keyof Events, Set<Function>>();
on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
// unsubscribe関数を返す
return () => {
this.listeners.get(event)?.delete(handler);
};
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
this.listeners.get(event)?.forEach((handler) => handler(payload));
}
once<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void): void {
const unsubscribe = this.on(event, (payload) => {
unsubscribe();
handler(payload);
});
}
}
// 使用例
const emitter = new TypedEventEmitter<AppEvents>();
emitter.on("user:login", ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
emitter.emit("user:login", { userId: "u123", timestamp: new Date() }); // OK
// emitter.emit("user:login", { userId: 123 }); // コンパイルエラー!
Command パターン(Undo/Redo対応)
interface Command<State> {
execute(state: State): State;
undo(state: State): State;
readonly description: string;
}
class CommandHistory<State> {
private undoStack: Command<State>[] = [];
private redoStack: Command<State>[] = [];
constructor(private state: State) {}
execute(command: Command<State>): void {
this.state = command.execute(this.state);
this.undoStack.push(command);
this.redoStack = []; // 新しいコマンド実行でredoスタックをクリア
}
undo(): boolean {
const command = this.undoStack.pop();
if (!command) return false;
this.state = command.undo(this.state);
this.redoStack.push(command);
return true;
}
redo(): boolean {
const command = this.redoStack.pop();
if (!command) return false;
this.state = command.execute(this.state);
this.undoStack.push(command);
return true;
}
getState(): Readonly<State> {
return this.state;
}
}
モダンTypeScriptパターン
Result型(エラーハンドリング)
例外ではなく型でエラーを表現するパターンです。
type Result<T, E = Error> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: E };
const Ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const Err = <E>(error: E): Result<never, E> => ({ ok: false, error });
// ユーティリティ関数
function mapResult<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> {
return result.ok ? Ok(fn(result.value)) : result;
}
async function tryCatch<T>(fn: () => Promise<T>): Promise<Result<T, Error>> {
try {
return Ok(await fn());
} catch (error) {
return Err(error instanceof Error ? error : new Error(String(error)));
}
}
// 使用例
async function fetchUser(id: string): Promise<Result<User, ApiError>> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return Err({ code: response.status, message: response.statusText });
}
const data = await response.json();
return Ok(data as User);
}
// 呼び出し側
const result = await fetchUser("123");
if (result.ok) {
console.log(result.value.name); // 型推論でUserが確定
} else {
console.error(result.error.code); // 型推論でApiErrorが確定
}
Branded Types(名目型)
プリミティブ型に意味的な区別を付与し、型の取り違えを防止します。
// Brand型の定義
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
// コンストラクタ関数(バリデーション付き)
function createUserId(value: string): UserId {
if (!value.startsWith("usr_")) throw new Error("Invalid UserId");
return value as UserId;
}
function createEmail(value: string): Email {
if (!value.includes("@")) throw new Error("Invalid email");
return value as Email;
}
// 関数定義で型の取り違えを防止
function getUser(id: UserId): Promise<User> { /* ... */ }
function getOrder(id: OrderId): Promise<Order> { /* ... */ }
const userId = createUserId("usr_123");
const orderId = "ord_456" as OrderId;
getUser(userId); // OK
// getUser(orderId); // コンパイルエラー!OrderIdはUserIdに代入不可
Dependency Injection(DIコンテナ)
// トークンベースのDI
const TOKENS = {
Logger: Symbol("Logger"),
Database: Symbol("Database"),
UserRepository: Symbol("UserRepository"),
UserService: Symbol("UserService"),
} as const;
// 簡易DIコンテナ
class Container {
private bindings = new Map<symbol, () => unknown>();
bind<T>(token: symbol, factory: () => T): void {
this.bindings.set(token, factory);
}
resolve<T>(token: symbol): T {
const factory = this.bindings.get(token);
if (!factory) throw new Error(`No binding for ${token.toString()}`);
return factory() as T;
}
}
// 登録
const container = new Container();
container.bind(TOKENS.Logger, () => new ConsoleLogger());
container.bind(TOKENS.Database, () => new PostgresDatabase(config));
container.bind(TOKENS.UserRepository, () =>
new UserRepository(container.resolve(TOKENS.Database))
);
container.bind(TOKENS.UserService, () =>
new UserService(
container.resolve(TOKENS.UserRepository),
container.resolve(TOKENS.Logger)
)
);
パターン選定ガイド
ユースケース別推奨パターン
| ユースケース | 推奨パターン | 理由 |
|---|---|---|
| 外部API統合 | Adapter | インターフェース差異の吸収 |
| 設定管理 | Module Singleton | シンプルで十分 |
| 複雑なオブジェクト生成 | Builder(型追跡付き) | 必須項目の設定漏れ防止 |
| アルゴリズム切替 | Strategy | 実行時の振る舞い変更 |
| イベント処理 | Observer(Typed) | 疎結合なコンポーネント連携 |
| エラーハンドリング | Result型 | 型安全なエラー伝搬 |
| ID管理 | Branded Types | 型の取り違え防止 |
| テスタビリティ | DI | モック注入の容易化 |
| Undo/Redo | Command | 操作の履歴管理 |
パターン適用時のアンチパターン
以下のような場合、パターンの過剰適用が起きている可能性があります。
- 過度な抽象化: 1つしか実装がないのにインターフェースを定義している
- パターンのためのパターン: コードが複雑化しているが、得られるメリットが少ない
- 型パズル: 型定義が複雑すぎてチームメンバーが理解できない
- 不必要なDI: 依存関係が単純なのにDIコンテナを導入している
テスト容易性を高めるパターン
Repository パターン + DI
// インターフェースの定義
interface UserRepository {
findById(id: UserId): Promise<User | null>;
save(user: User): Promise<void>;
findByEmail(email: string): Promise<User | null>;
}
// 本番実装
class PostgresUserRepository implements UserRepository {
constructor(private readonly db: Database) {}
async findById(id: UserId): Promise<User | null> {
return this.db.query("SELECT * FROM users WHERE id = $1", [id]);
}
async save(user: User): Promise<void> {
await this.db.query(
"INSERT INTO users (id, name, email) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET name = $2, email = $3",
[user.id, user.name, user.email]
);
}
async findByEmail(email: string): Promise<User | null> {
return this.db.query("SELECT * FROM users WHERE email = $1", [email]);
}
}
// テスト用モック
class InMemoryUserRepository implements UserRepository {
private users = new Map<string, User>();
async findById(id: UserId): Promise<User | null> {
return this.users.get(id) ?? null;
}
async save(user: User): Promise<void> {
this.users.set(user.id, user);
}
async findByEmail(email: string): Promise<User | null> {
return [...this.users.values()].find((u) => u.email === email) ?? null;
}
}
まとめ
TypeScriptのデザインパターンを活用することで、以下のメリットが得られます。
- コンパイル時の安全性: 型システムがパターンの構造を保証
- 高い保守性: インターフェースが仕様書として機能
- テスト容易性: DI・Repositoryパターンによるモック容易化
- チーム開発の効率化: 型がコミュニケーションツールになる
- リファクタリングの安全性: 型エラーが変更の影響を即座に通知
2026年のTypeScript開発では、GoFパターンの知識に加えて、Result型やBranded Typesなどの型レベルプログラミングの手法を組み合わせることが重要です。パターンは目的ではなく手段であり、チームの理解度とプロジェクトの複雑さに応じて適切に選択することが、堅牢なアプリケーション構築への道です。
関連記事
- 【2026年版】AI コーディングアシスタント比較
- 【2026年版】Next.js × AI チャットボット構築チュートリアル
- 【2026年版】Python FastAPI マイクロサービス構築ガイド
- 【2026年版】RAG実装ガイド