TypeScript 宣言の併合
1. Declaration Merging(宣言の併合)の理解
Declaration Merging(宣言の併合)は、同じ名前を持つ複数の宣言を一つの定義に結合する TypeScript の強力な機能です。 これにより、複雑な型を段階的に構築したり、既存の型を型安全(Type-safe)な方法で拡張したりすることが可能になります。
1.1 主なメリット
- 漸進的な強化(Progressive Enhancement): 複数の宣言にわたって型を段階的に構築できます。
- 拡張性(Extensibility): 元の定義を修正することなく、既存の型に新しいメンバーを追加できます。
- コードの整理(Organization): 巨大な型定義を論理的なグループに分割して管理できます。
- 互換性(Compatibility): 必要に応じてサードパーティの型定義を拡張できます。
1.2 一般的なユースケース
- ビルトイン型やサードパーティライブラリの型拡張
- JavaScript ライブラリへの型情報の追加
- 複数のファイルにわたる大規模なインターフェースの整理
- メソッドチェーンを利用した流れるようなインターフェース(Fluent API)の実装
- モジュール拡張(Module Augmentation)パターンの実装
2. インターフェースの併合(Interface Merging)
同じ名前のインターフェースは自動的に併合されます。
コード例
// 最初の宣言
interface Person {
name: string;
age: number;
}
// 同名での2番目の宣言
interface Person {
address: string;
email: string;
}
// TypeScript はこれらを以下のように併合します:
// interface Person {
// name: string;
// age: number;
// address: string;
// email: string;
// }
const person: Person = {
name: "John",
age: 30,
address: "123 Main St",
email: "[email protected]"
};
console.log(person);3. 併合による関数のオーバーロード
複数の関数宣言を定義し、後に実装時にそれらを併合させることができます。
コード例
// 関数のオーバーロード(Overloads)
function processValue(value: string): string;
function processValue(value: number): number;
function processValue(value: boolean): boolean;
// すべてのオーバーロードを処理する実装
function processValue(value: string | number | boolean): string | number | boolean {
if (typeof value === "string") {
return value.toUpperCase();
} else if (typeof value === "number") {
return value * 2;
} else {
return !value;
}
}
// 異なる型での関数利用
console.log(processValue("hello")); // "HELLO"
console.log(processValue(10)); // 20
console.log(processValue(true)); // false4. 名前空間の併合(Namespace Merging)
同じ名前を持つ名前空間(Namespace)も併合されます。
コード例
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
}
namespace Validation {
export interface NumberValidator {
isValid(n: number): boolean;
}
export class ZipCodeValidator implements StringValidator {
isValid(s: string): boolean {
return s.length === 5 && /^\d+$/.test(s);
}
}
}
// 併合後:
// namespace Validation {
// export interface StringValidator { isValid(s: string): boolean; }
// export interface NumberValidator { isValid(n: number): boolean; }
// export class ZipCodeValidator implements StringValidator { ... }
// }
// 併合された名前空間の使用
const zipValidator = new Validation.ZipCodeValidator();
console.log(zipValidator.isValid("12345")); // true
console.log(zipValidator.isValid("1234")); // false
console.log(zipValidator.isValid("abcde")); // false5. クラスとインターフェースの併合
クラス宣言は、同じ名前のインターフェースと併合することができます。
コード例
// インターフェース宣言
interface Cart {
calculateTotal(): number;
}
// 同名のクラス宣言
class Cart {
items: { name: string; price: number }[] = [];
addItem(name: string, price: number): void {
this.items.push({ name, price });
}
// インターフェースのメソッドを実装する必要がある
calculateTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
// 併合されたクラスとインターフェースの使用
const cart = new Cart();
cart.addItem("Book", 15.99);
cart.addItem("Coffee Mug", 8.99);
console.log(`Total: $${cart.calculateTotal().toFixed(2)}`);6. Enum の併合
同じ名前の Enum(列挙型)宣言は併合されます。
コード例
// Enum の最初の部分
enum Direction {
North,
South
}
// Enum の2番目の部分
enum Direction {
East = 2,
West = 3
}
// 併合後:
// enum Direction {
// North = 0,
// South = 1,
// East = 2,
// West = 3
// }
console.log(Direction.North); // 0
console.log(Direction.South); // 1
console.log(Direction.East); // 2
console.log(Direction.West); // 3
// 値によるアクセスも可能
console.log(Direction[0]); // "North"
console.log(Direction[2]); // "East"7. モジュール拡張(Module Augmentation)
追加の型や機能を宣言することで、既存のモジュールやライブラリを拡張できます。
コード例
// 元のライブラリ定義
// これがサードパーティライブラリから提供されていると仮定します
declare namespace LibraryModule {
export interface User {
id: number;
name: string;
}
export function getUser(id: number): User;
}
// 追加機能による拡張(自分のコード)
declare namespace LibraryModule {
// 新しいインターフェースを追加
export interface UserPreferences {
theme: string;
notifications: boolean;
}
// 既存のインターフェースに新しいプロパティを追加
export interface User {
preferences?: UserPreferences;
}
// 新しい関数を追加
export function getUserPreferences(userId: number): UserPreferences;
}
// 拡張されたモジュールの使用
const user = LibraryModule.getUser(123);
console.log(user.preferences?.theme);
const prefs = LibraryModule.getUserPreferences(123);
console.log(prefs.notifications);8. ベストプラクティス
Declaration Merging を使用する際には、いくつか考慮すべきルールがあります。
- オーバーロードの順序: 関数のオーバーロードでは順序が重要です。実装のシグネチャ(Signature)は最も汎用的なものである必要があります。
- 非関数メンバーの互換性: 2つのインターフェースが同名のプロパティを宣言する場合、それらの型は同一または互換性がなければなりません。
- 後から定義されたインターフェースの優先順位: 併合されたインターフェース内で競合が発生した場合、基本的には後の宣言が優先されます。
- Private および Protected メンバー: クラスが同じ名前の
privateまたはprotectedメンバーを持ち、それらの型が異なる場合、併合はできません。 - 名前空間のエクスポート: 併合後、名前空間の外側からは
exportされた宣言のみが可視となります。
9. パフォーマンスに関する考慮事項
- コンパイル時間: 過度な Declaration Merging は、コンパイル時間を増大させる可能性があります。
- 型チェック: 複雑に併合された型は、IDE のパフォーマンスに影響を与える場合があります。
- バンドルサイズ: Declaration Merging はコンパイル時の概念であるため、実行時のパフォーマンスやバンドルサイズには影響しません。
9.1 最適化のヒント
- 併合するインターフェースは焦点が絞られ、まとまりのある(Cohesive)状態に保ちます。
- 併合された型の中での深いネストは避けます。
- 単純な型の組み合わせであれば、マージではなく型エイリアス(Type Alias)の使用を検討してください。