こんにちは!TypeScriptを触り始めると、「既存のライブラリにある型に、自分のプロパティを追加したいな…」という壁にぶつかることがよくありますよね。
他の言語、例えばJavaやC#などのオブジェクト指向言語で育った方だと、「既存のクラスを勝手に拡張するなんて、モンキーパッチみたいで危なそう…」と思われるかもしれません。でも、TypeScriptには、この問題を美しく、かつ安全に解決するための強力な仕組みが用意されています。
それが今回解説する「インターフェースの宣言マージ(Declaration Merging)」です。
ここをクリアすれば、サードパーティ製ライブラリの型定義も思いのままに操れるようになりますよ。さあ、一緒にその仕組みをマスターしていきましょう!
—
1. 宣言マージってなに?(イメージ図解)
まず、「宣言マージ」という名前の由来から見ていきましょう。
通常の変数や定数は、同じ名前で何度も宣言すると「エラー(識別子が重複しています)」になりますよね。
しかし、`interface`(インターフェース)は特別です。同じ名前のインターフェースを複数回宣言すると、TypeScriptのコンパイラが裏側でそれらを「1つに合体(マージ)」してくれるという性質を持っています。
イメージとしては、こんな感じです。
[ あなたが書いた宣言 A ] + [ ライブラリ等の既存宣言 B ]
│ │
└──────────────┬──────────────┘
▼
【 魔法のコンパイラによるマージ 】
│
▼
[ 1つに合体した最強のインターフェース ]
このように、別々の場所で定義された同名のインターフェースが、コンパイル時にひとつの型として統合される。これが宣言マージの本質です。
—
2. 基本的な使い方を体験しよう
まずはシンプルな例で、どう動くのかを確かめてみましょう。
例えば、Webブラウザの `Window` オブジェクトや、独自のユーザー情報を表すインターフェースを拡張する場面を想像してください。
// 1つ目の宣言:ベースとなる型定義(例えばライブラリ側や共通定義)
interface User {
id: number;
name: string;
}
// 2つ目の宣言:別のファイルや自分のコードで、同じ名前のインターフェースを定義する
interface User {
email: string; // プロパティを追加している!
}
// 【結果】コンパイル後、User型は以下のように統合されたものとして扱われます
// interface User {
// id: number;
// name: string;
// email: string;
// }
const myUser: User = {
id: 1,
name: ‘Taro Yamada’,
email: ‘taro@example.com’, // おお!エラーにならずにプロパティを追加できました!
};
すごいですよね!同じ名前の `interface` を書くだけで、自動的にプロパティが合体しました。これが、TypeScriptの型システムが持つ奥深くて便利な機能です。
—
3. 【実践】サードパーティ製ライブラリの型を安全に拡張する
さて、ここからが本番です。実際の開発現場では、npmでインストールしたサードパーティ製ライブラリ(例えば、ExpressやAxiosなど)のオブジェクトに、独自のプロパティを追加したいというケースが多々あります。
ここでは、よくある「Expressの `Request` オブジェクトにログイン中のユーザー情報を追加したい」というシチュエーションを例に見てみましょう。
準備:モジュール拡張(Module Augmentation)の魔法
サードパーティ製ライブラリは、たいていの場合「モジュール(`import` / `export` を使う世界)」として作られています。モジュールの中にあるインターフェースを拡張するには、`declare global` や `declare module` という構文を使って、「グローバル空間や既存モジュールの型を書き換える宣言です!」とTypeScriptに教えてあげる必要があります。
// types/express.d.ts (プロジェクト内の型定義ファイルなど)
// Expressのモジュールを対象に拡張を行うことを宣言します
import ‘express’;
// モジュールの内部型を書き換えるための構文
declare module ‘express’ {
// Requestインターフェースを拡張する
interface Request {
// ログインしているユーザーの情報を追加
user?: {
id: string;
role: ‘admin’ | ‘user’;
};
}
}
// これにより、このファイル以降(またはプロジェクト全体)で
// expressのRequest型に `user` プロパティが自動的に生えてきます!
なぜこれが安全なの?(実行時とコンパイル時の分離)
「でもこれ、実行時には本当に動くの?」と不安になるかもしれません。
安心してください。TypeScriptの `interface` は、コンパイルが終わった(JavaScriptに変換された)時には跡形もなく消え去ります。
つまり、これは実行時のJavaScriptの挙動を書き換えているわけではなく、「TypeScriptのコンパイラに対する『このオブジェクトにはこういうプロパティが入ってくる予定だから、型チェックで怒らないでね』という約束事(契約)」を追加しているだけなのです。
実際の実行時には、ミドルウェア(認証チェックなど)が `req.user` に実際に値を代入するように実装しておけば、型安全と実行時安全の両方が美しく両立します。
—
4. 陥りやすい文法エラーと注意点 ⚠️
ここで、初学者が非常によくハマる「罠」をいくつかご紹介しておきます。これを覚えておくだけで、無駄なエラーに悩まされる時間が激減しますよ!
罠その1:`type`(型エイリアス)でやろうとして爆死する
「インターフェースがマージできるなら、型エイリアス(`type`)でも同じことができるはず!」と思っていませんか?
// ❌ やってはいけない例
type Point = { x: number };
type Point = { y: number }; // 💥 エラー!「識別子 ‘Point’ は重複しています。」
【知見の核心】
`type` キーワードで定義される型エイリアスは、単なる「別の名前の付与(Alias)」であり、宣言マージの対象外です。重複した名前を許しません。
「拡張したいなら `interface`、ユニオン型やプリミティブの別名なら `type`」 という明確な使い分けの基準がここにあるのです。
罠その2:プロパティの「上書き」で型が矛盾する
すでに存在するプロパティの型を、宣言マージで無理やり変えようとすると、コンパイラに怒られます。
interface Box {
value: string;
}
// ❌ 既存のプロパティの型を勝手に変えることはできない
interface Box {
value: number; // エラー:型 ‘number’ は型 ‘string’ のプロパティ ‘value’ と互換性がありません
}
宣言マージは、あくまで「新しいプロパティの追加」や「メソッドのオーバーロード(同名関数の引数違いの定義)」を行うためのものです。既存の型を壊すような変更はできない(=型安全性が守られている)ようになっています。
—
まとめ:TypeScriptの型を味方につけよう
いかがでしたか?今回は「Interfaceの宣言マージ」について、その仕組みと実践的な使い方を解説しました。
- 宣言マージとは?
同じ名前の `interface` を複数書くことで、コンパイル時に自動的にプロパティが合体する機能。
- 何に使うの?
サードパーティ製ライブラリ(Expressなど)の型定義に、自前でカスタムプロパティを追加したいときに大活躍する。
- 注意点は?
`type`(型エイリアス)ではマージできないので、拡張性を考慮するなら最初は `interface` を選ぶのが定石。
ここをクリアできれば、もうTypeScriptの型システムで恐いものはありません。サードパーティの型定義が足りなくても、「自分で拡張してしまえばいいや!」と余裕を持って開発できるようになりますよ。
日々のコーディングが、少しでも知的で楽しいものになりますように。それではまた、次の記事でお会いしましょう!