はじめに
TypeScriptのユニークな概念のいくつかでは、型レベルでJavaScriptオブジェクトの形状を記述します。特にTypeScriptに固有の例の1つが、「宣言のマージ」の概念です。この概念を理解すると、既存のJavaScriptを扱う際に有利になります。また、より高度な抽象化の概念への道も開かれます。
この記事の目的上、「宣言のマージ」とは、コンパイラが同じ名前で宣言された2つの別々の宣言を1つの定義にマージすることを意味します。このマージされた定義には、元の宣言の両方の機能があります。任意の数の宣言をマージできます。2つの宣言に限定されるものではありません。
基本概念
TypeScriptでは、宣言は少なくとも名前空間、型、または値の3つのグループのうち1つにエンティティを作成します。名前空間作成宣言は、ドット表記を使用してアクセスされる名前を含む名前空間を作成します。型作成宣言は、宣言された形状で表示され、指定された名前にバインドされる型を作成します。最後に、値作成宣言は、出力JavaScriptに表示される値を作成します。
宣言タイプ | 名前空間 | 型 | 値 |
---|---|---|---|
名前空間 | X | X | |
クラス | X | X | |
列挙型 | X | X | |
インターフェース | X | ||
型エイリアス | X | ||
関数 | X | ||
変数 | X |
各宣言で何が作成されるかを理解すると、宣言マージを実行するときに何がマージされるかを理解するのに役立ちます。
インターフェースのマージ
最も単純で、おそらく最も一般的なタイプの宣言マージは、インターフェースマージです。最も基本的なレベルでは、マージは、両方の宣言のメンバーを、同じ名前の単一のインターフェースに機械的に結合します。
ts
interface Box {height: number;width: number;}interface Box {scale: number;}let box: Box = { height: 5, width: 6, scale: 10 };
インターフェースの関数以外のメンバーは一意である必要があります。一意でない場合は、同じ型である必要があります。インターフェースが両方とも同じ名前の関数以外のメンバーを宣言しているが、異なる型である場合、コンパイラはエラーを発行します。
関数メンバーの場合、同じ名前の各関数メンバーは、同じ関数のオーバーロードを記述するものとして扱われます。また、インターフェースA
が後続のインターフェースA
とマージする場合、2番目のインターフェースは最初のインターフェースよりも優先度が高くなることに注意してください。
つまり、例では
ts
interface Cloner {clone(animal: Animal): Animal;}interface Cloner {clone(animal: Sheep): Sheep;}interface Cloner {clone(animal: Dog): Dog;clone(animal: Cat): Cat;}
3つのインターフェースは、次のような1つの宣言を作成するためにマージされます。
ts
interface Cloner {clone(animal: Dog): Dog;clone(animal: Cat): Cat;clone(animal: Sheep): Sheep;clone(animal: Animal): Animal;}
各グループの要素は同じ順序を維持しますが、グループ自体は後続のオーバーロードセットが最初に順序付けられてマージされることに注意してください。
この規則の例外の1つは、特殊化されたシグネチャです。シグネチャに、型が単一の文字列リテラル型(例:文字列リテラルの和集合ではない)であるパラメーターがある場合、マージされたオーバーロードリストの先頭にバブルアップされます。
たとえば、次のインターフェースは一緒にマージされます。
ts
interface Document {createElement(tagName: any): Element;}interface Document {createElement(tagName: "div"): HTMLDivElement;createElement(tagName: "span"): HTMLSpanElement;}interface Document {createElement(tagName: string): HTMLElement;createElement(tagName: "canvas"): HTMLCanvasElement;}
Document
の結果としてマージされた宣言は次のようになります。
ts
interface Document {createElement(tagName: "canvas"): HTMLCanvasElement;createElement(tagName: "div"): HTMLDivElement;createElement(tagName: "span"): HTMLSpanElement;createElement(tagName: string): HTMLElement;createElement(tagName: any): Element;}
名前空間のマージ
インターフェースと同様に、同じ名前の名前空間もメンバーをマージします。名前空間は名前空間と値の両方を作成するため、両方がどのようにマージされるかを理解する必要があります。
名前空間をマージするには、各名前空間で宣言されたエクスポートされたインターフェースからの型定義自体がマージされ、内部にマージされたインターフェース定義を持つ単一の名前空間を形成します。
名前空間の値をマージするには、各宣言サイトで、指定された名前の名前空間が既に存在する場合、既存の名前空間を取得し、2番目の名前空間のエクスポートされたメンバーを最初の名前空間に追加することでさらに拡張されます。
この例での Animals
の宣言のマージは
ts
namespace Animals {export class Zebra {}}namespace Animals {export interface Legged {numberOfLegs: number;}export class Dog {}}
以下と同等です
ts
namespace Animals {export interface Legged {numberOfLegs: number;}export class Zebra {}export class Dog {}}
この名前空間のマージのモデルは、役立つ出発点ですが、エクスポートされていないメンバーで何が起こるかも理解する必要があります。エクスポートされていないメンバーは、元の(マージされていない)名前空間でのみ表示されます。つまり、マージ後、他の宣言から来たマージされたメンバーは、エクスポートされていないメンバーを表示できません。
この例でより明確に確認できます
ts
namespace Animal {let haveMuscles = true;export function animalsHaveMuscles() {return haveMuscles;}}namespace Animal {export function doAnimalsHaveMuscles() {return haveMuscles; // Error, because haveMuscles is not accessible here}}
haveMuscles
はエクスポートされていないため、同じマージされていない名前空間を共有する animalsHaveMuscles
関数のみがシンボルを表示できます。doAnimalsHaveMuscles
関数は、マージされた Animal
名前空間の一部であっても、このエクスポートされていないメンバーを表示できません。
クラス、関数、列挙型との名前空間のマージ
名前空間は、他の型の宣言ともマージできるほど柔軟性があります。そのためには、名前空間の宣言は、マージする宣言に従う必要があります。結果の宣言には、両方の宣言型のプロパティがあります。TypeScriptは、この機能を使用して、JavaScriptや他のプログラミング言語のいくつかのパターンをモデル化します。
クラスとの名前空間のマージ
これにより、ユーザーは内部クラスを記述する方法が得られます。
ts
class Album {label: Album.AlbumLabel;}namespace Album {export class AlbumLabel {}}
マージされたメンバーの可視性ルールは、「名前空間のマージ」セクションで説明した内容と同じであるため、マージされたクラスが表示されるように AlbumLabel
クラスをエクスポートする必要があります。最終結果は、別のクラス内で管理されるクラスです。名前空間を使用して、既存のクラスにさらに静的メンバーを追加することもできます。
内部クラスのパターンに加えて、JavaScriptで関数を作成し、その関数にプロパティを追加してさらに拡張するという習慣にも精通しているかもしれません。TypeScriptは宣言のマージを使用して、このような定義をタイプセーフな方法で構築します。
ts
function buildLabel(name: string): string {return buildLabel.prefix + name + buildLabel.suffix;}namespace buildLabel {export let suffix = "";export let prefix = "Hello, ";}console.log(buildLabel("Sam Smith"));
同様に、名前空間を使用して列挙型を静的メンバーで拡張できます
ts
enum Color {red = 1,green = 2,blue = 4,}namespace Color {export function mixColor(colorName: string) {if (colorName == "yellow") {return Color.red + Color.green;} else if (colorName == "white") {return Color.red + Color.green + Color.blue;} else if (colorName == "magenta") {return Color.red + Color.blue;} else if (colorName == "cyan") {return Color.green + Color.blue;}}}
許可されないマージ
すべてのマージがTypeScriptで許可されているわけではありません。現在、クラスは他のクラスや変数とマージできません。クラスのマージを模倣する方法については、「TypeScriptのミックスイン」セクションを参照してください。
モジュールの拡張
JavaScriptモジュールはマージをサポートしていませんが、既存のオブジェクトをインポートして更新することでパッチを適用できます。簡単なObservableの例を見てみましょう
ts
// observable.tsexport class Observable<T> {// ... implementation left as an exercise for the reader ...}// map.tsimport { Observable } from "./observable";Observable.prototype.map = function (f) {// ... another exercise for the reader};
これはTypeScriptでも問題なく動作しますが、コンパイラーは Observable.prototype.map
について知りません。モジュールの拡張を使用して、コンパイラーに通知できます
ts
// observable.tsexport class Observable<T> {// ... implementation left as an exercise for the reader ...}// map.tsimport { Observable } from "./observable";declare module "./observable" {interface Observable<T> {map<U>(f: (x: T) => U): Observable<U>;}}Observable.prototype.map = function (f) {// ... another exercise for the reader};// consumer.tsimport { Observable } from "./observable";import "./map";let o: Observable<number>;o.map((x) => x.toFixed());
モジュール名は、import
/export
のモジュール指定子と同じように解決されます。詳細については、「モジュール」を参照してください。次に、拡張機能の宣言は、元の宣言と同じファイルで宣言されたかのようにマージされます。
ただし、留意すべき2つの制限事項があります
- 拡張機能では、新しいトップレベルの宣言を宣言することはできません。既存の宣言へのパッチのみです。
- 既定のエクスポートも拡張できません。名前付きエクスポートのみです(エクスポートをエクスポートされた名前で拡張する必要があり、
default
は予約語であるため - 詳細については、#14080 を参照してください)
グローバル拡張
モジュール内からグローバルスコープに宣言を追加することもできます
ts
// observable.tsexport class Observable<T> {// ... still no implementation ...}declare global {interface Array<T> {toObservable(): Observable<T>;}}Array.prototype.toObservable = function () {// ...};
グローバル拡張には、モジュール拡張と同じ動作と制限があります。