Skip to content

デザインパターンと実践テクニック

デザインパターンと実践テクニック 💡

Section titled “デザインパターンと実践テクニック 💡”

TypeScriptは、単なるJavaScriptのスーパーセットではなく、アプリケーションの設計を改善するための多くの機能を提供します。ここでは、TypeScriptを最大限に活用するためのデザインパターンと実践的なテクニックを解説します。

なぜデザインパターンが重要なのか

Section titled “なぜデザインパターンが重要なのか”

問題のあるコード(型安全性の欠如):

// 問題: 型情報が不足している
function processOrder(order) {
if (order.status === "pending") {
return order.total * 0.1; // 10%の手数料
}
return 0;
}
// 実行時エラーの可能性
processOrder({ status: "completed", total: "100" }); // NaNが返される

解決: 型安全な設計パターン

// 解決: 型安全な設計
type OrderStatus = "pending" | "completed" | "cancelled";
interface Order {
id: number;
status: OrderStatus;
total: number;
}
function processOrder(order: Order): number {
if (order.status === "pending") {
return order.total * 0.1;
}
return 0;
}
// コンパイル時にエラーが検出される
processOrder({ status: "completed", total: "100" }); // エラー: totalはnumber型である必要がある

1. インターフェースと型エイリアスの使い分け

Section titled “1. インターフェースと型エイリアスの使い分け”

interfacetypeはどちらも型を定義できますが、それぞれの特性を理解して使い分けることが重要です。

  • interface: オブジェクトの構造や、クラスが実装すべきコントラクトを定義するために使います。

    • 拡張性: extendsキーワードを使って、他のインターフェースを拡張できます。
    • 宣言のマージ: 同じ名前のインターフェースを複数宣言すると、それらが自動的にマージされます。これは、ライブラリが既存の型に新しいプロパティを追加したい場合に便利です。
  • type (型エイリアス): プリミティブ型、ユニオン型、インターセクション型など、任意の型に別名を付けたい場合に最適です。

    • 柔軟性: interfaceではできない複雑な型(例:ユニオン型)を定義できます。

使い分けのベストプラクティス:

Section titled “使い分けのベストプラクティス:”

オブジェクトの型を定義する場合、クラスのように振る舞う構造を定義するならinterface、単なる型の別名として使うならtypeを検討します。

一般的には、オブジェクトの型定義には**interfaceを優先し、他の型(ユニオン型など)を組み合わせる必要がある場合にtypeを使用**するのが良いでしょう。

関数オーバーロードは、同じ関数名で異なる引数の型や数に対応する機能です。TypeScriptは、呼び出し時の引数に基づいて、どの関数の実装が使用されるかをコンパイル時に判断します。

// オーバーロードシグネチャ
function add(x: string, y: string): string;
function add(x: number, y: number): number;
// 実装シグネチャ(より汎用的な型を使用)
function add(x: any, y: any): any {
return x + y;
}
// 呼び出し例
const sum1 = add("hello", " world"); // string 型と推論
const sum2 = add(1, 2); // number 型と推論

このテクニックは、柔軟なAPIを設計する際に役立ちます。

型ガードは、特定のスコープ内で変数の型を絞り込むための技術です。これにより、unknownやユニオン型を安全に扱えます。

  • typeof 型ガード: プリミティブ型をチェックします。
  • instanceof 型ガード: クラスのインスタンスかどうかをチェックします。
  • ユーザー定義型ガード: 開発者が独自の型チェック関数を定義します。
interface Cat { meow(): void; }
interface Dog { bark(): void; }
type Pet = Cat | Dog;
// ユーザー定義型ガード関数
function isCat(pet: Pet): pet is Cat {
return (pet as Cat).meow !== undefined;
}
function speak(pet: Pet) {
if (isCat(pet)) {
pet.meow(); // ここでは pet は Cat 型として扱われる
} else {
(pet as Dog).bark(); // もしくは pet.bark();
}
}

pet is Catという構文が、この関数がtrueを返す場合にpetCat型であることを保証します。

実践的なユースケース:

// ユースケース1: APIレスポンスの型ガード
interface SuccessResponse<T> {
success: true;
data: T;
}
interface ErrorResponse {
success: false;
error: string;
}
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
function isSuccess<T>(response: ApiResponse<T>): response is SuccessResponse<T> {
return response.success === true;
}
async function fetchUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
const result = await fetchUser(1);
if (isSuccess(result)) {
// ここではresultはSuccessResponse<User>型
console.log(result.data.name); // 型安全
} else {
// ここではresultはErrorResponse型
console.error(result.error); // 型安全
}

4. ブランド型(Nominal Typing)の実装

Section titled “4. ブランド型(Nominal Typing)の実装”

TypeScriptは構造的部分型を採用していますが、時には名目型(Nominal Typing)が必要な場合があります。ブランド型を使用することで、同じ構造を持つ型を区別できます。

実践的なユースケース:

// ユースケース1: 単位の区別
type USD = number & { readonly __brand: unique symbol };
type EUR = number & { readonly __brand: unique symbol };
function createUSD(amount: number): USD {
return amount as USD;
}
function createEUR(amount: number): EUR {
return amount as EUR;
}
function addUSD(a: USD, b: USD): USD {
return (a + b) as USD;
}
const usd1 = createUSD(100);
const usd2 = createUSD(50);
const eur1 = createEUR(100);
addUSD(usd1, usd2); // OK
addUSD(usd1, eur1); // エラー: EUR型はUSD型に割り当てられない
// ユースケース2: IDの型安全性
type UserId = number & { readonly __brand: unique symbol };
type OrderId = number & { readonly __brand: unique symbol };
function createUserId(id: number): UserId {
return id as UserId;
}
function getUserById(id: UserId): User {
// 実装
return {} as User;
}
const userId = createUserId(1);
const orderId = 1 as OrderId;
getUserById(userId); // OK
getUserById(orderId); // エラー: OrderId型はUserId型に割り当てられない

実践的なユースケース:

// ユースケース: 型安全なイベントエミッター
type EventMap = {
userCreated: { id: number; name: string };
userUpdated: { id: number; changes: Partial<User> };
userDeleted: { id: number };
};
class TypedEventEmitter {
private listeners: {
[K in keyof EventMap]?: Array<(data: EventMap[K]) => void>;
} = {};
on<K extends keyof EventMap>(
event: K,
listener: (data: EventMap[K]) => void
): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
const listeners = this.listeners[event];
if (listeners) {
listeners.forEach(listener => listener(data));
}
}
}
const emitter = new TypedEventEmitter();
// 型安全なイベントリスナー
emitter.on("userCreated", (data) => {
console.log(data.id, data.name); // 型安全
});
emitter.on("userUpdated", (data) => {
console.log(data.id, data.changes); // 型安全
});
// エラー: イベント名とデータの型が一致しない
emitter.emit("userCreated", { id: 1, changes: {} }); // エラー

実践的なユースケース:

// ユースケース: 型安全なステートマシン
type State =
| { type: "idle" }
| { type: "loading" }
| { type: "success"; data: User }
| { type: "error"; error: string };
type StateType = State["type"];
class StateMachine {
private state: State = { type: "idle" };
transition(newState: State): void {
this.state = newState;
}
getState(): State {
return this.state;
}
// 型安全な状態チェック
isIdle(): this is { state: { type: "idle" } } {
return this.state.type === "idle";
}
isSuccess(): this is { state: { type: "success"; data: User } } {
return this.state.type === "success";
}
}
const machine = new StateMachine();
if (machine.isSuccess()) {
// ここではstate.dataがUser型として推論される
console.log(machine.getState().data.name); // 型安全
}

実践的なユースケース:

// ユースケース: 型安全なクエリビルダー
interface QueryBuilder<T> {
where<K extends keyof T>(
field: K,
operator: "eq" | "ne" | "gt" | "lt",
value: T[K]
): QueryBuilder<T>;
orderBy<K extends keyof T>(field: K, direction: "asc" | "desc"): QueryBuilder<T>;
limit(count: number): QueryBuilder<T>;
build(): string;
}
class TypedQueryBuilder<T> implements QueryBuilder<T> {
private conditions: string[] = [];
private orderByClause: string = "";
private limitClause: string = "";
where<K extends keyof T>(
field: K,
operator: "eq" | "ne" | "gt" | "lt",
value: T[K]
): this {
this.conditions.push(`${String(field)} ${operator} ${value}`);
return this;
}
orderBy<K extends keyof T>(field: K, direction: "asc" | "desc"): this {
this.orderByClause = `ORDER BY ${String(field)} ${direction}`;
return this;
}
limit(count: number): this {
this.limitClause = `LIMIT ${count}`;
return this;
}
build(): string {
const where = this.conditions.length > 0
? `WHERE ${this.conditions.join(" AND ")}`
: "";
return [where, this.orderByClause, this.limitClause]
.filter(Boolean)
.join(" ");
}
}
interface User {
id: number;
name: string;
age: number;
}
const query = new TypedQueryBuilder<User>()
.where("age", "gt", 18) // 型安全: ageはnumber型
.where("name", "eq", "Alice") // 型安全: nameはstring型
.orderBy("age", "desc")
.limit(10)
.build();

実践的なユースケース:

// ユースケース: 型安全なDIコンテナ
type Constructor<T> = new (...args: any[]) => T;
class Container {
private services = new Map<Constructor<any>, any>();
register<T>(ctor: Constructor<T>, instance: T): void {
this.services.set(ctor, instance);
}
resolve<T>(ctor: Constructor<T>): T {
const instance = this.services.get(ctor);
if (!instance) {
throw new Error(`Service ${ctor.name} not found`);
}
return instance as T;
}
}
class UserService {
getUsers(): User[] {
return [];
}
}
class OrderService {
getOrders(): Order[] {
return [];
}
}
const container = new Container();
container.register(UserService, new UserService());
container.register(OrderService, new OrderService());
// 型安全な解決
const userService = container.resolve(UserService);
const users = userService.getUsers(); // User[]型として推論

これらの実践的なテクニックを習得することで、TypeScriptの強力な型システムをフル活用し、より堅牢で保守しやすいコードを記述できます。

シニアエンジニアとして考慮すべき点:

  1. 型安全性と柔軟性のバランス: 過度に厳格な型定義は開発速度を低下させる可能性がある
  2. パフォーマンス: 複雑な型はコンパイル時間に影響する可能性がある
  3. チームの理解度: チーム全体が理解できるレベルの型定義を心がける
  4. 段階的な導入: 既存のコードベースに段階的に型安全性を追加する