コードレビューをしていて、次のようなコードに遭遇したことはないだろうか。
type Options = {
url: string;
timeout?: number;
};
function sendRequest(options: Options) {
// …
}
// 呼び出し側
sendRequest({
url: ‘https://api.example.com’,
timout: 5000, // ⚠ スペルミスだが、構造的部分型によりコンパイルを通ってしまう
});
フロントエンド開発やAPIクライアントの設計において、この「タイポ」や「不要なプロパティの混入」がコンパイルをすり抜ける現象は、実に多くのバグを生み出してきた。原因はTypeScriptの根幹である構造的部分型付け(Structural Subtyping)にある。TypeScriptは「必要なプロパティさえ満たしていれば、余計なものが含まれていてもよい」という原則で型を評価するためだ。
しかし、プロダクションコードの堅牢性を極限まで高めるアーキテクトであれば、この挙動を野放しにはしない。今回は、オブジェクトリテラルに潜む「余剰プロパティ」を型レベルで完全に封殺し、コンパイラに厳格な検閲を行わせるための実践的な設計パターンを伝授する。
—
なぜ通常の型定義では不十分なのか?
先ほどの例を思い出してほしい。`Options` 型に対して `{ url: string; timout: number }` というオブジェクトリテラルを直接渡した場合、TypeScriptはオブジェクトリテラル割当則(Excess Property Checking)を発動し、通常は未知のプロパティ(`timout`)を検知してエラーにしてくれる。
しかし、このチェックが働くのは「オブジェクトリテラルを直接関数の引数に渡す場合」という非常に狭い条件下のみである。
以下のようなケースを見てほしい。
const userConfig = {
url: ‘https://api.example.com’,
timout: 5000, // スペルミス
retries: 3, // 謎の独自プロパティ
};
// 変数を経由した瞬間、Excess Property Checkingは消失する
sendRequest(userConfig); // ❌ エラーにならない!
変数を経由したり、外部から渡ってきた設定オブジェクトを扱う場合、構造的部分型付けの特性により、`Options` が要求するプロパティさえあれば、余剰なプロパティは完全にスルーされてしまう。これが大規模アプリケーションや複雑なコンポーネント設計において、隠れたバグの温床となる。
—
解決策:型パズルによる「厳格な余剰プロパティ排除」
この問題を根本から解決するためには、「渡されたオブジェクトの型から、期待される型のキーを引いた差分が空(never)であること」をコンパイル時に強制するジェネリック制約を設計すればよい。
実務でそのまま使えるプロダクションコードを見ていこう。
/
- 厳格なオブジェクト型制約ユーティリティ
- T: 許可されたプロパティを持つベース型
/
type StrictOptions
? {
[K in keyof U]: K extends keyof T ? T[K] : never;
}
: never;
// または、よりシンプルかつ強力に余剰プロパティを排除するアプローチ
type NoExcessProperties
[K in Exclude
};
しかし、上記をそのまま関数のシグネチャに適用すると、エラーメッセージが非常に難解になり、開発者体験(DX)が損なわれる。
チーフアーキテクトとして推奨するのは、「Mapped Types」と「条件付き型(Conditional Types)」を組み合わせた、エラーメッセージが明快なアプローチだ。
実装パターン:コンポーネント・API設定の厳格化
以下のコードは、実務の現場で即座に応用できる完成形の設計パターンである。
// 1. ベースとなる設定型
type ApiConfig = {
endpoint: string;
method: ‘GET’ | ‘POST’;
timeout?: number;
};
// 2. 余剰プロパティをコンパイルエラーにするためのヘルパー型
// 期待されるキー以外のプロパティが含まれている場合、型レベルで値を never に強制する
type Exactly
[K in keyof U]: K extends keyof T ? T[K] : never;
};
// 3. ジェネ릭を用いた関数シグネチャの定義
// U(実際に渡された型)が T(ベース型)の構造に厳密に一致することを強制する
function executeApiCall
console.log(`Executing ${config.method} to ${config.endpoint}`);
}
// ==========================================
// 【検証】コンパイル結果の挙動
// ==========================================
// ✅ 正常系:正しいプロパティのみ
executeApiCall({
endpoint: ‘/api/v1/users’,
method: ‘GET’,
timeout: 3000,
});
// ❌ 異常系1:タイポの検知 (method)
executeApiCall({
endpoint: ‘/api/v1/users’,
method: ‘GET’, // Error: Type ‘{ method: string; … }’ is not assignable to type ‘never’.
timeout: 3000,
} as const);
// ❌ 異常系2:未定義の余剰プロパティの混入 (cache)
executeApiCall({
endpoint: ‘/api/v1/users’,
method: ‘GET’,
cache: true, // Error: Object literal may only specify known properties, and ‘cache’ does not exist in type…
});
—
アーキテクトが解説する:この設計の深層とパフォーマンス
なぜこのアプローチが優れているのか、そして実務で導入する際の注意点を言語化しておこう。
1. 型評価のメカニズム
`T extends ApiConfig, U extends Exactly
2. 開発者体験(DX)とエラーメッセージのバランス
高度な型パズルを導入しすぎると、IDE(VSCode等)のホバー時に表示される型ヒントが複雑化し、「何が間違っているのか」がエンジニアに伝わりにくくなる。
上記の `Exactly
3. パフォーマンス上の注意点(コンパイル速度)
複雑な conditional types や mapped types を多用しすぎると、TypeScriptの型チェックエンジン(TSServer)のメモリ消費量が増加し、大規模なコードベースにおいてビルド時間が数秒〜数十秒単位で遅延する原因になる。
この厳格な型チェックを適用すべき箇所は、「SDKのパブリックAPI」「共通コンポーネントのProps」「基盤となるHTTPクライアントの設定」など、バグが致命傷になり得る境界線(Boundary)に限定すべきだ。すべての内部関数にこれを適用するのは過剰設計(Over-engineering)である。
—
まとめ:型は「制約」ではなく「ドキュメントであり契約」である
優れたTypeScriptコードとは、単にコンパイルエラーが出ないコードではない。「間違ったコードを書くことが物理的に不可能な状態を作り上げる」ことこそが、型システムを極めるアーキテクトの仕事である。
今回紹介した余剰プロパティの排除テクニックをAPIクライアントやデザインシステムのコンポーネント設計に取り入れることで、コードレビューでの「プロパティ名のタイポ指摘」という不毛な時間をゼロにし、より本質的なビジネスロジックの設計に集中できる環境を手に入れてほしい。