TypeScriptのインターフェースを「ハック」する:構造的部分型を武器にした最強のMock戦略
フロントエンド開発の現場で、APIレスポンスやドメインモデルの巨大な `interface` に頭を抱えたことはないだろうか。
「テストのためにこの50個のプロパティを持つインターフェースをすべて埋めなければならないのか?」
多くのエンジニアはここで妥協し、`as any` や `as unknown as Type` といった「型システムの自殺行為」に走る。しかし、TypeScriptの核心である「構造的部分型(Structural Subtyping)」を正しく理解していれば、そんな暴力的な回避策は不要だ。
今日は、コンパイラの裏側を味方につけ、堅牢かつ最小限の記述でMockを生成する「型安全なハック」を伝授する。
—
1. なぜ「構造的部分型」がMockの救世主なのか
TypeScriptは名前(Nominal)ではなく「形(Structure)」で型を評価する。つまり、ターゲットとなるインターフェースが要求するプロパティさえ満たしていれば、それ以上のプロパティがあろうと、クラスが異なろうと、コンパイラはそれを「同一の型」と見なす。
この特性を逆手に取れば、「必要な部分だけを抜き出したサブセット」を、元の巨大なインターフェースと型レベルで一致させることが可能だ。
—
2. 実践:PartialとPickを組み合わせた「Mockファクトリー」
よくあるアンチパターンは、各テストファイルで毎回巨大なオブジェクトをリテラルで書くことだ。これでは、インターフェースが変更された際にテストが壊れる(あるいはテストが壊れず、偽の安心感だけが残る)。
以下のパターンを導入せよ。
/
- 巨大なドメインモデル
/
interface UserProfile {
id: string;
name: string;
email: string;
settings: { theme: ‘dark’ | ‘light’; notifications: boolean };
createdAt: Date;
lastLogin: Date;
// 他にも数十のプロパティが…
}
/
- 魔法のユーティリティ:Partialを再帰的に適用し、
- かつ任意のプロパティを上書き可能にするファクトリー
/
type MockFactory
// 実務で使えるMock生成器
const createMockUser: MockFactory
id: ‘test-id’,
name: ‘Test User’,
email: ‘test@example.com’,
settings: { theme: ‘light’, notifications: true },
createdAt: new Date(),
lastLogin: new Date(),
…overrides, // ここで差分を注入
});
この設計の何が優れているか
1. 保守性の担保: `UserProfile` にプロパティが追加されても、ファクトリーの初期値が欠けている場合、コンパイラが即座にエラーを出す。これにより「テスト用のMockが仕様から乖離する」という最悪の事態を防げる。
2. 疎結合: 特定のテストで特定のプロパティのみが必要な場合、`createMockUser({ name: ‘Special Name’ })` と書くだけでいい。
—
3. 「部分的な実装」を型安全に強制するテクニック
もし、インターフェースが巨大すぎて、テストのたびに初期値を書くのすら面倒な場合は、`DeepPartial` を活用しつつ、「必要なものだけを実装する」というアプローチを採るべきだ。
/
- 再帰的にPartialにするユーティリティ
/
type DeepPartial
[P in keyof T]?: T[P] extends object ? DeepPartial
};
// 使用例:必要な箇所だけを注入する
const partialUser: DeepPartial
settings: { theme: ‘dark’ }
};
// 型アサーションを最小限に抑えるためのラッパー関数を噛ませる
const mockUser = (partial: DeepPartial
警告: `as UserProfile` を使うことに罪悪感を覚えるかもしれない。しかし、これは「このテストスコープ内では、このオブジェクトは少なくともこの形をしていることを保証する」という契約(Assertion)である。`any` を使うのとはわけが違う。
—
4. パフォーマンスとコンパイル負荷の最適化
複雑なMapped Types(`DeepPartial`など)を多用すると、プロジェクトが巨大化した際にコンパイル時間が顕著に悪化することがある。
- 避けるべきこと: 型定義ファイルの中で複雑な計算(条件付き型など)をネストしすぎること。
- 推奨すること: Mock生成用の型は、`Pick` や `Omit` を使い、深さを浅く保つこと。
// 悪い例:再帰が深すぎる
type DeepDeepDeep
// 良い例:必要なデータ構造を抽出して個別に型を作る
type UserSettings = UserProfile[‘settings’];
const createMockSettings = (o: Partial
—
結論:型システムは「縛るもの」ではなく「守るもの」
TypeScriptの型システムは、コードを縛り付けるための檻ではない。巨大で複雑なWebアプリケーションという混沌の中で、「何がどこにあり、どう振る舞うべきか」を言語化するための地図だ。
Mockを生成する際、安易な `any` に逃げるのは、その地図を破り捨てる行為に等しい。構造的部分型を理解し、`Partial` や `Pick` を適切に使いこなすことで、あなたのテストコードは「壊れにくい」だけでなく、仕様変更を歓迎する「しなやかなコード」へと進化するはずだ。
次は、あなたのプロジェクトの `interface` を眺めてみてほしい。そこには、まだあなたが活用していない「構造の美学」が眠っているはずだ。