Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

TypeScript declares a merge


May 07, 2021 TypeScript


Table of contents


TypeScript declares a merge

Introduced

There are some unique concepts in TypeScript that describe the model of JavaScript objects at a type level. A particularly unique example of this is the concept of "declaration consolidation". U nderstanding this concept will help you manipulate the existing JavaScript code. It also helps to understand more advanced abstract concepts.

For this file, "declaration merge" means that the compiler merges two separate declarations for the same name into a single declaration. T he merged declaration has the characteristics of both previous declarations. Any number of declarations can be merged;

The basic concept

Declarations in Typescript create one of three entities: namespace, type, or value. T he declaration that creates the namespace creates a new namespace that contains the name used when accessing with the (.) symbol. T he declaration of the type created is to create a type with the declared model and bind it to a given name. Finally, creating a declaration of values creates the values that you see in the JavaScript output.

Declaration Type Namespace Type Value
Namespace
Class
Enum
Interface
Type Alias
Function
Variable

Understanding what each claim creates helps you understand what is merged when the claims are merged.

Merge the interface

The simplest and most common type of declaration merge is interface merge. Fundamentally, the mechanism of consolidation is to put members of both parties in an interface of the same name.

interface Box {
    height: number;
    width: number;
}

interface Box {
    scale: number;
}

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

The non-function members of the interface must be unique. If both interfaces declare a non-function member compiler with the same name, an error is reported.

For function members, each function declaration with the same name is treated as an overload of the function. It is also important to note that when A is merged with later A the subsequent interface has a higher priority.

Here's an example:

interface Cloner {
    clone(animal: Animal): Animal;
}

interface Cloner {
    clone(animal: Sheep): Sheep;
}

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

The three interfaces are combined into one declaration:

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

Note that the declaration order in each set of interfaces remains the same, but the order between the groups of interfaces is that subsequent interface overloads appear at the top.

One exception to this rule is when a special function signature occurs. If the type of argument in the signature is a single literal string (for example, not a union type of literal string), it will be promoted to the top of the overloaded list.

For example, the following interfaces are merged together:

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;
}

The Document will look something like this:

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

Merge namespaces

Similar to interfaces, namespaces with the same name merge their members. Namespaces create namespaces and values, and we need to know how they are combined.

For the merge of namespaces, the interfaces of the same name exported by the module are merged to form a single namespace that contains the merged interfaces.

For the merge of values in a namespace, if a namespace for a given name currently exists, the exported members of the later namespace are added to the module that already exists.

Animals merge examples:

namespace Animals {
    export class Zebra { }
}

namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}

Equivalent to:

namespace Animals {
    export interface Legged { numberOfLegs: number; }

    export class Zebra { }
    export class Dog { }
}

In addition to these merges, you also need to understand how non-exported members are handled. N on-exported members are only visible in their original (pre-merged) namespace. This means that after a merge, members merged from other namespaces cannot access non-exported members.

The following example provides a clearer explanation:

namespace Animal {
    let haveMuscles = true;

    export function animalsHaveMuscles() {
        return haveMuscles;
    }
}

namespace Animal {
    export function doAnimalsHaveMuscles() {
        return haveMuscles;  // <-- error, haveMuscles is not visible here
    }
}

Because haveMuscles exported, only the animalsHaveMuscles the original un merged namespace to access this variable. doAnimalsHaveMuscles part of the merged namespace, but it does not have access to unexisted members.

Namespaces are merged with classes and functions and enumeration types

Namespaces can be merged with other types of declarations. A s long as the definition of the namespace conforms to the definition of the type to be merged. T he combined result contains the types of claims for both. Typescript uses this feature to implement some of the design patterns in JavaScript.

Merge namespaces and classes

This allows us to represent internal classes.

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

The merge rules are consistent 合并命名空间 in the Merge Namespace section above, and we must export the AlbumLabel class so that the merged class can access it. T he result of the merge is a class with an internal class. You can also use namespaces to add some static properties to your class.

In addition to the internal class pattern, it is also common for you to create a function in JavaScript and later extend it to add some properties. Typescript uses declaration merges to do this and to keep the type safe.

function buildLabel(name: string): string {
    return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
    export let suffix = "";
    export let prefix = "Hello, ";
}

alert(buildLabel("Sam Smith"));

Similarly, namespaces can be used to extend enumerals:

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;
        }
    }
}

Illegal merger

TypeScript does not allow all merges. C urrently, classes cannot be merged with other classes or variables. To learn how to mimic class merges, refer to TypeScript's Blending.

Module extension

Although JavaScript does not support merges, you can patch imported objects to update them. Let's take a look at this toy example:

// observable.js
export class Observable<T> {
    // ... implementation left as an exercise for the reader ...
}

// map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
    // ... another exercise for the reader
}

It also works well in TypeScript, but the compiler knows Observable.prototype.map You can use the extension to tell the compiler:

// observable.ts stays the same
// 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) {
    // ... another exercise for the reader
}


// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());

The resolution of the module name is consistent with the export module identifier is resolved with import F or more information, please refer to Modules. W hen these claims are merged in an extension, it is as if they were declared in their original location. However, you cannot declare a new top-level declaration in an extension -- you can only extend a claim that already exists in the module.

Global extension

You also add declarations to the global scope by adding them inside the module.

// observable.ts
export class Observable<T> {
    // ... still no implementation ...
}

declare global {
    interface Array<T> {
        toObservable(): Observable<T>;
    }
}

Array.prototype.toObservable = function () {
    // ...
}

Global extensions have the same behavior and limitations as module extensions.