こんにちは!TypeScriptの型定義、毎日お疲れ様です。
「`string`や`number`はわかるようになったけれど、なんだか複雑な書き方が出てくると途端に手が止まってしまう……」
そんなふうに悩んでいませんか?大丈夫ですよ、みんな最初はそこで立ち止まります。私も昔は、エラー画面を見るだけでそっとブラウザのタブを閉じたくなりました。
さて、今回はTypeScriptの中級への扉を開く、ちょっと魔法のような機能「Declaration Merging(宣言の結合:インターフェースの結合)」についてお話しします。
難しそうな名前がついていますが、安心してください。身近な例えを使いながら、ゆっくり紐解いていきましょう。
—
そもそも「インターフェースの結合」ってなに?
いきなりコードを書く前に、ちょっと想像してみてください。
あなたは今、自分専用の「お道具箱」を持っています。最初はこのお道具箱に「ハサミ」と「ノリ」だけを入れていました。
しばらく経って、「やっぱりホッチキスも追加したいな」と思いました。でも、わざわざ新しいお道具箱を買いに行きますか? あるいは、今までのお道具箱を全部バラして作り直しますか?
めんどくさいですよね。普通は、「今あるお道具箱に、後からホッチキスをスポッと追加する」はずです。
TypeScriptの「インターフェースの結合」は、まさにこれと同じことをやってくれます。
同じ名前のインターフェースをバラバラの場所で定義すると、TypeScriptが勝手に中身を合体させて一つにしてくれるという、とってもおせっかいで優しい機能なんです。
—
基本の形を見てみよう
百聞は一見にしかず、まずはコードを見てみましょう。
たとえば、ユーザーのプロフィール情報を表す `User` というインターフェースを作ったとします。
// 1つ目の定義:最初からある基本のお道具箱
interface User {
name: string; // お名前
age: number; // ご年齢
}
// 2つ目の定義:あとから「やっぱり住所も追加しよう!」と定義する
interface User {
address: string; // ご住所(後から追加!)
}
「あれ? 同じ `User` って名前でインターフェースを2回作ってる……これってエラーにならないの?」って思いますよね。
普通の変数だったら `Identifier ‘User’ has already been declared.` なんて怒られてしまいます。
しかし、インターフェース(`interface`)の力にかかれば、この2つは自動的に合体します。
結果として、TypeScriptはこの `User` を以下のように認識してくれます。
// TypeScriptの頭の中では、こう合体している!
interface User {
name: string;
age: number;
address: string; // ちゃんと合わさっている!
}
実際に使ってみると、こんなふうに3つのプロパティすべてを要求されるようになります。
const myProfile: User = {
name: ‘TypeScript太郎’,
age: 3,
address: ‘東京都渋谷区’ // これがないと「足りないよ!」って怒られます
};
すごいですよね!あとからポコッとプロパティを追加できるなんて、まるでレゴブロックのようです。
—
なぜこの機能が実務で役に立つの?(ライブラリの型拡張)
「へえ、面白い機能だけど、自分で書くなら最初から1つの場所に全部書けばよくない?」
鋭いですね!その通り、自分でコードを上から下まで書く場合は、わざわざ分けるメリットはあまりありません。
この「結合」が真価を発揮するのは、「自分以外の誰かが作ったライブラリ(外部のパッケージ)の型をちょっと改造したいとき」です。
例えば、有名なフロントエンドのフレームワークや、便利なライブラリをインストールしたとします。そのライブラリの中に、もともとこういう型が入っていたとしましょう。
// ライブラリがあらかじめ用意している設定ファイル(触れないものとする)
interface AppConfig {
theme: ‘light’ | ‘dark’;
}
「このライブラリ、すごく便利なんだけど、自分のプロジェクトでは独自に `apiKey` っていう設定も追加で持たせたいんだよな……。でも、ライブラリのソースコードを直接書き換えるわけにはいかないし……」
そんな絶望的な場面でこそ、インターフェースの結合の出番です!
自分のプロジェクトの適当なファイル(たとえば `types.d.ts` など)に、こう書くだけでいいのです。
// 自分のプロジェクトのファイルで、ライブラリと同じ名前のインターフェースを宣言する
interface AppConfig {
apiKey: string; // 「うちのプロジェクト用に追加させてね!」
}
これだけで、ライブラリにもともとあった `theme` に加えて、あなたが追加した `apiKey` も `AppConfig` の一員として認められます。これが実務でめちゃくちゃよく使われる「型の拡張(Module Augmentationの基礎)」の考え方です。
サードパーティ製のライブラリが「型が足りないよ〜」とケチなことを言ってきたら、この結合の術を思い出してくださいね。
—
つまずきやすいポイントと注意点
とっても便利な結合の仕組みですが、初心者のうちはちょっとだけハマりやすいポイントがあります。そっとフォローしておきますね。
1. `type` エイリアスではできない!
「じゃあ、`type User = { … }` でも同じように合体できるの?」と思った方、残念ながら `type`(タイプエイリアス)ではこれはできません。
`type User` は同じ名前で2回定義すると、「重複してるよ!」と容赦なくエラーになります。
「結合できるのは `interface` だけ」という点は、テストにもよく出る(?)重要ポイントです。
2. 同じプロパティ名で型が違うと喧嘩しちゃう
もし、こんな風に定義してしまったらどうなるでしょう?
interface Box {
size: number;
}
interface Box {
size: string; // あれ?さっきは number だったのに……?
}
これはTypeScriptくんも困惑してしまいます(型の不一致エラーになります)。
「追加」はできますが、すでに存在するプロパティの型を勝手に上書き(変更)することは原則としてできませんのでご注意を。
—
まとめ
いかがでしたでしょうか?
Declaration Merging(インターフェースの結合)について、少しイメージが湧いてきたでしょうか?
- 同じ名前の `interface` を複数書くと、中身が自動的に合体する!
- 自分で全部書くときよりも、外部の型を拡張したいときに真価を発揮する!
- `type` ではできなくて、`interface` だけの特権!
TypeScriptの型定義は、最初は厳格すぎて窮屈に感じるかもしれませんが、仕組みが分かってくると「こっちの意図を汲み取って守ってくれる、頼もしい相棒」に変わってきます。
焦らず、一歩ずつ自分のペースで進んでいきましょう。あなたのTypeScriptライフを、心から応援しています!

コメント