メインコンテンツへ移動

宣言マージ_TypeScript ノート 16

無料2019-04-21#TypeScript#TypeScript扩展声明#TypeScript重写声明#TypeScript修改声明#TypeScript mixin#TypeScript声明合并规则

宣言マージの意義は、既存の(タイプ)宣言を拡張できることです

一。概要

CSS の宣言マージに類似しています:

.box {
  background: red;
}
.box {
  color: white;
}

/* 等価 */
.box {
  background: red;
  color: white;
}

TypeScript にもこのようなメカニズムがあります:

interface IPerson {
  name: string;
}
interface IPerson {
  age: number;
}

// 等価
interface IPerson {
  name: string;
  age: number;
}

簡単に言えば、同じものを記述する複数の宣言は 1 つにマージされます

二。基本概念

TypeScript では、1 つの宣言がネームスペース、タイプ、または値を作成する可能性があります。例えば Class を宣言すると、同時にタイプと値が作成されます:

class Greeter {
  static standardGreeting = "Hello, there";
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter: Greeter; // Greeter タイプ
greeter = new Greeter("world"); // Greeter 値

([クラスとタイプ](/articles/クラス-typescript ノート 4/#articleHeader8) より抜粋)

したがって、宣言を 3 つのカテゴリに分類できます:

  • ネームスペースを作成する宣言:ドット(.)でアクセスするネームスペース名を作成

  • タイプを作成する宣言:指定された「形状」のタイプを作成し、与えられた名前で命名

  • 値を作成する宣言:値を作成し、出力される JavaScript にも存在

具体的には、TypeScript の 7 種類の宣言の中で、ネームスペースはネームスペースと値の意味を持ち、クラスと列挙は同時にタイプと値の意味を持ち、インターフェースとタイプエイリアスはタイプのみ、関数と変数は値のみの意味を持ちます:

Declaration TypeNamespaceTypeValue
NamespaceXX
ClassXX
EnumXX
InterfaceX
Type AliasX
FunctionX
VariableX

三。インターフェースのマージ

最もシンプルで、最も一般的な宣言マージはインターフェースマージで、基本ルールは同名インターフェースのメンバーを一緒にすることです:

interface Box {
  height: number;
  width: number;
}
interface Box {
  scale: number;
}
// 等価
interface MergedBox {
  height: number;
  width: number;
  scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};
let b: MergedBox = box;

非関数メンバーは一意である必要があります。一意でない場合、タイプが同じ関数メンバーは無視され、タイプが異なる場合はコンパイルエラーがスローされます:

interface Box {
  color: string
}
// エラー Subsequent property declarations must have the same type.
interface Box {
  color: number
}

関数メンバーの場合、同名のものは [関数オーバーロード](/articles/関数-typescript ノート 5/#articleHeader9) とみなされます:

class Animal { }
class Sheep { }
class Dog { }
class Cat { }
interface Cloner {
  clone(animal: Animal): Animal;
}
interface Cloner {
  clone(animal: Sheep): Sheep;
}
interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
}

これはマージされます:

interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
  clone(animal: Sheep): Sheep;
  clone(animal: Animal): Animal;
}

同一宣言内のマージ後は宣言順序を維持し、異なる宣言間では後宣言が優先されます(つまり、後のインターフェース宣言文で定義された関数メンバーはマージ結果で前になります)。非関数メンバーはマージ後、辞書順に並べられます

特別に、関数シグネチャが文字列リテラルタイプのパラメータを含む場合、マージ後のオーバーロードリストの先頭に配置されます:

interface IDocument {
  createElement(tagName: any): Element;
}
interface IDocument {
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
}
interface IDocument {
  createElement(tagName: string): HTMLElement;
  createElement(tagName: "canvas"): HTMLCanvasElement;
}

マージ結果:

interface IDocument {
  // 特殊シグネチャを先頭に
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
  createElement(tagName: "canvas"): HTMLCanvasElement;
  // 以下の 2 つは引き続き後宣言優先に従う
  createElement(tagName: string): HTMLElement;
  createElement(tagName: "canvas"): HTMLCanvasElement;
}

四。ネームスペースのマージ

インターフェースと同様に、複数の同名ネームスペースもメンバーマージが発生します。特別な点はネームスペースが値の意味も持つことで、状況が少し複雑になります

  • ネームスペースマージ:各(同名)ネームスペースが公開するインターフェースをマージし、同時に単一ネームスペース内部でもインターフェースマージ

  • 値マージ:後宣言のネームスペースから公開されたメンバーを先宣言に追加

例えば:

namespace Animals {
  export class Zebra { }
}
namespace Animals {
  export interface Legged { numberOfLegs: number; }
  export class Dog { }
}
// 等価
namespace Animals {
  export interface Legged { numberOfLegs: number; }

  export class Zebra { }
  export class Dog { }
}

特別に、公開されていないメンバー(non-exported member)はソースネームスペース内でのみ可視 です(ネームスペースマージメカニズムが存在しても):

namespace Animal {
  let haveWings = true;

  export function animalsHaveWings() {
    return haveWings;
  }
}
namespace Animal {
  export function doAnimalsHaveWings() {
    // エラー Cannot find name 'haveWings'.
    return haveWings;
  }
}

ネームスペースはスコープ分離を持つため、公開されていないメンバーはネームスペースに挂载されません:

var Animal;
(function (Animal) {
  var haveWings = true;
  function animalsHaveWings() {
    return haveWings;
  }
  Animal.animalsHaveWings = animalsHaveWings;
})(Animal || (Animal = {}));
(function (Animal) {
  function doAnimalsHaveWings() {
    // エラー Cannot find name 'haveWings'.
    return haveWings;
  }
  Animal.doAnimalsHaveWings = doAnimalsHaveWings;
})(Animal || (Animal = {}));

クラス、関数および列挙とのマージ

他のネームスペースとマージできるだけでなく、ネームスペースはクラス、関数および列挙ともマージできます

この能力により、既存のクラス、関数、列挙を(タイプ上で)拡張でき、JavaScript の一般的なパターンを記述するために使用されます。例えばクラスに静的メンバーを追加したり、関数に静的属性を追加したりなど

P.S.ネームスペース宣言は後に出現する必要があります。否则エラー:

// エラー A namespace declaration cannot be located prior to a class or function with which it is merged.
namespace A {
  function f() { }
}
class A {
  fn() { }
}

マージではなくオーバーライドが発生するため:

// コンパイル結果
var A;
(function (A) {
  function f() { }
})(A || (A = {}));
var A = /** @class */ (function () {
  function A() {
  }
  A.prototype.fn = function () { };
  return A;
}());

クラスとのマージ

ネームスペースを通じて既存の Class に静的メンバーを追加できます。例えば:

class Album {
  label: Album.AlbumLabel;
}
namespace Album {
  export class AlbumLabel { }
}

ネームスペース間のマージルールと一致するため、class AlbumLabel を公開して、他の宣言のメンバーがアクセスできるようにする必要があります

関数とのマージ

ネームスペースとクラスのマージと同様に、関数とのマージは既存の関数に静的属性を拡張できます:

function buildLabel(name: string): string {
  return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
  export let suffix = "";
  export let prefix = "Hello, ";
}
// test
buildLabel('Lily') === "Hello, Lily"

列挙とのマージ

enum Color {
  red = 1,
  green = 2,
  blue = 4
}
namespace Color {
  export function mixColor(colorName: string) {
    if (colorName == "yellow") {
      return Color.red + Color.green;
    }
    else {
      return -1;
    }
  }
}

// test
Color.mixColor('white');

列挙に静的メソッドを持たせるのは奇妙に見えますが、JavaScript には実際に類似のシナリオが存在し、属性セットに動作を追加するようなものです:

// JavaScript
const Color = {
  red: 1,
  green: 2,
  blue: 4
};
Color.mixColor = function(colorName) {/* ... */};

五.Class Mixin

クラス宣言は他のクラスや変数宣言とマージしませんが、Class Mixin を通じて類似の効果を達成できます:

function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach(baseCtor => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
      Object.defineProperty(derivedCtor.prototype, name, Object.getOwnPropertyDescriptor(baseCtor.prototype, name));
    });
  });
}

ツール関数を通じて他のクラスプロトタイプ上のものをターゲットクラスプロトタイプに貼り付け、他のクラスの能力(動作)を持たせます:

class Editable {
    public value: string;
    input(s: string) { this.value = s; }
}
class Focusable {
    focus() { console.log('Focused'); }
    blur() { console.log('Blured'); }
}
// 他のクラスからタイプを取得
class Input implements Editable, Focusable {
    // 実装待ちの Editable インターフェース
    value: string;
    input: (s: string) => void;
    // 実装待ちの Focusable インターフェース
    focus: () => void;
    blur: () => void;
}
// 他のクラスから動作を取得
applyMixins(Input, [Editable, Focusable]);

// log 'Focused'
new Input().focus();

P.S. その中のimplements Editable, Focusable はソースクラスのタイプを取得し、インターフェースに類似しています。詳細は Interfaces Extending Classes を参照

六。モジュール拡張

// ソースファイル observable.js
export class Observable {
  constructor(source) { this.source = source; }
}

// ソースファイル map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
  return new Observable(f(this.source));
}

このモジュール拡張方式は JavaScript で非常に一般的ですが、TypeScript ではエラーが報告されます:

// ソースファイル observable.ts
export class Observable<T> {
  constructor(public source: T) { }
}

// ソースファイル map.ts
import { Observable } from "./observable";
// エラー Property 'map' does not exist on type 'Observable<any>'.
Observable.prototype.map = function (f) {
  return new Observable(f(this.source));
}

この時、モジュール拡張(module augmentation)を通じてコンパイラ(タイプシステム)にモジュールに追加されたメンバーを通知できます:

// ソースファイル map.ts
import { Observable } from "./observable";
// モジュール拡張
declare module "./observable" {
    interface Observable<T> {
        map<U>(f: (x: T) => U): Observable<U>;
    }
}
Observable.prototype.map = function (f) {/* ... */}

その中で、モジュール名の解析方式はimport/export と一致し、詳細は [モジュール解析メカニズム_TypeScript ノート 14](/articles/モジュール解析メカニズム-typescript ノート 14/) を参照。モジュール宣言に追加された拡張メンバーはソースモジュールにマージされます(まるで元々同じファイルに宣言されていたかのように)。この方式で既存のモジュールを拡張できますが、2 つの制限 があります:

  • モジュール拡張にトップレベル宣言を追加できず、既存の宣言のみ拡張可能

  • デフォルトエクスポートを拡張できず、名前付きエクスポートのみ拡張可能(default は予約語のため、名前で拡張できないため。詳細は Can not declaration merging for default exported class を参照)

P.S. 上記の例は Playground などの環境でdeclare module "./observable" エラーに遭遇する可能性があります:

Invalid module name in augmentation, module './observable' cannot be found.

Ambient module declaration cannot specify relative module name.

モジュールファイルが存在しないことが原因で、実際のファイルモジュールでは正常にコンパイルできます

グローバル拡張

同様の方式で「グローバルモジュール」(つまりグローバルスコープ下のものを修正)も拡張できます。例えば:

// ソースファイル observable.ts
export class Observable<T> {
  constructor(public source: T) { }
}
declare global {
  interface Array<T> {
    toObservable(): Observable<T>;
  }
}
Array.prototype.toObservable = function () {
  return new Observable(this);
}

declare global はグローバルスコープを拡張することを示し、追加されたものはArray などのグローバル宣言にマージされます

参考資料

コメント

コメントはまだありません

コメントを書く