一。概要
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 Type | Namespace | Type | Value |
|---|---|---|---|
| Namespace | X | X | |
| Class | X | X | |
| Enum | X | X | |
| Interface | X | ||
| Type Alias | X | ||
| Function | X | ||
| Variable | X |
三。インターフェースのマージ
最もシンプルで、最も一般的な宣言マージはインターフェースマージで、基本ルールは同名インターフェースのメンバーを一緒にすることです:
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 などのグローバル宣言にマージされます
コメントはまだありません