TypeScriptの「宣言マージ」という諸刃の剣をあえて封じる:堅牢な型設計への招待
こんにちは。TypeScriptの深淵を日々探索している皆さんに、今日は少し「玄人好み」の、でも現場で最も重要なトピックをお届けします。
TypeScriptには「宣言マージ(Declaration Merging)」という強力な機能があります。同じ名前のインターフェースを複数回書くと、それらが「合体」して一つの定義になる魔法のような仕組みです。ライブラリ開発では重宝しますが、大規模なアプリケーション開発では、この「魔法」が時に悪夢のようなバグを招くことがあります。
今日は、あえてこの宣言マージを禁止し、「閉じた型定義」を作ることで、あなたのプロジェクトをより強固にする方法を解説します。
—
1. 宣言マージの「便利さ」と「危険性」
まずは、宣言マージが何をしているのか、図でイメージしてみましょう。
// 1つ目の定義
interface User {
id: number;
}
// 2つ目の定義(同じ名前!)
interface User {
name: string;
}
// 結果:Userは { id: number; name: string; } として評価される
const user: User = { id: 1, name: “Alice” };
これ、一見便利ですよね。しかし、大規模開発では「誰かがどこかでこっそりインターフェースを拡張してしまい、予期せぬプロパティが紛れ込む」という事態が起こります。特にチーム開発では、この「勝手な拡張」が型安全性をジワジワと蝕んでいくのです。
—
2. なぜ「宣言マージ」を禁止すべきなのか?
型定義がどこまでも広がってしまうと、「この型には本当は何が含まれているのか?」を追跡するのが困難になります。
- 意図しない副作用: 意図せずプロパティが増え、バリデーションをすり抜ける。
- コードジャンプの混乱: エディタで定義元に飛ぼうとしても、あちこちのファイルに定義が分散していて全体像が見えない。
これらは、開発者の認知負荷を爆発させる原因になります。「型は、定義された場所で完結しているべき」。これがクリーンアーキテクチャへの第一歩です。
—
3. 宣言マージを禁止する「型エイリアス」の選択
実は、宣言マージを避ける最も簡単な方法は、`interface` ではなく `type` (型エイリアス) を使うことです。
type User = {
id: number;
};
// エラー!「識別子 ‘User’ は重複しています」
// TypeScriptはここで「君はもう定義したよね?」と厳しく止めてくれます。
type User = {
name: string;
};
`type` を使えば、同じ名前を二重に定義することは物理的に不可能です。これにより、コードの意図が強制的に明確化されます。
—
4. それでもインターフェースを使いたい場合の設定
「プロジェクトの規約として `interface` を使いたいが、宣言マージだけは防ぎたい」というケースもあるでしょう。実は、コンパイラ設定でこれを完全に防ぐ魔法のようなフラグはありませんが、tsconfig.json を工夫して厳格さを高めることは可能です。
厳格な型チェックを維持する設計指針
1. `noImplicitAny` を有効にする: 拡張されたプロパティが意図せず `any` になるのを防ぎます。
2. `exactOptionalPropertyTypes` を有効にする: 存在しないプロパティを代入するミスをコンパイル時に検知します。
3. ESLintの導入: `typescript-eslint` を使い、`@typescript-eslint/no-redeclare` ルールを有効にすることで、インターフェースの再宣言をエディタ上で即座にエラーにできます。
// .eslintrc.json
{
“rules”: {
“@typescript-eslint/no-redeclare”: “error”
}
}
—
5. 現場で生きる「閉じた型設計」の考え方
もし、型を拡張したいという欲求が生まれたら、それは「インターフェースのマージ」ではなく、「継承」や「合成」を使うべきサインです。
// 基本となる型
interface BaseUser {
id: number;
}
// 継承によって明確に拡張する(拡張元がハッキリしている)
interface AdminUser extends BaseUser {
role: ‘admin’;
}
このように書けば、`BaseUser` を勝手に変えることはできません。誰かがコードを読んだときに「ああ、AdminUserはBaseUserを拡張したものなんだな」と一目瞭然ですよね。
—
まとめ:型は「意図」を閉じ込める箱である
TypeScriptの神髄は、単にコードを動かすことではなく、「コードの意図をコンパイラに正しく伝えること」にあります。
- インターフェースは宣言マージという「拡張性」を持つが、その分リスクも高い。
- 型エイリアス(type)は「閉じた定義」を強制し、再宣言を許さない。
- 拡張したいときは、継承や合成を使って関係性を可視化する。
ここを意識するだけで、あなたの書くTypeScriptコードの信頼性は劇的に向上します。まずは、プロジェクトのインターフェースを一つ、`type` に置き換えることから始めてみてください。その「きっちりとした感覚」こそが、一流のエンジニアへの入り口ですよ。
これからも、TypeScriptの深層を一緒に探索していきましょう!応援しています。