This is the 29th day of my participation in the August Wenwen Challenge.More challenges in August

There are some unique concepts in TypeScript that describe the model of JavaScript objects at the type level. A particularly unique example of this is the concept of “declarative merge”. Understanding this concept will help you manipulate existing JavaScript code. At the same time, it will be helpful to understand more advanced abstract concepts.

For the purposes of this document, “declaration merge” means that the compiler merges two separate declarations for the same name into a single declaration. The combined declaration has both the features of the original two declarations. Any number of declarations can be combined; Not limited to two declarations.

Basic concept

Declarations in TypeScript create one of three entities: a namespace, a type, or a value. The declaration to create a namespace creates a new namespace that contains the names used with (.) Symbol to use when accessing. The declaration for creating a type is to use the declared model to create a type and bind it to the given name. Finally, the statement that creates the value creates the value you see in the JavaScript output.

Combined interface

The simplest and most common type of declared merge is interface merge. Basically, the mechanism of merging is to put the members of both parties into an interface with the same name.

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

interface Box {
    scale: number;
}

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

Non-function members of an interface should be unique. If they are not unique, they must be of the same type. If two interfaces declare non-function members of the same name and their types are different, the compiler will report an error.

For function members, each function declaration with the same name is treated as an overload of that function. Note that when interface A is merged with subsequent interface A, the subsequent interface has higher priority.

As shown in the following example:

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

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

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

These 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;
}
Copy the code

Note that the order of declarations in each group of interfaces remains the same, but the order between the groups of interfaces is that later interface overloads appear at the front.

One exception to this rule is when a special function signature occurs. If one of the parameters in the signature is a string literal of type _ single _ (i.e., a joint type that is not a string literal), it will be promoted to the top of the overload 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;
}
Copy the code

The merged Document will look like this:

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

Merge namespace

Like 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 merging of namespaces, the interfaces exported by modules are merged to form a single namespace containing the merged interfaces.

For merging values in a namespace, if a namespace with the given name already exists, the exported members of the subsequent namespace are added to the module that already exists.

Animals declaration merge example:

namespace Animals {
    export class Zebra { }
}

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

Is equal to:

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

    export class Zebra { }
    export class Dog { }
}
Copy the code

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

The following example provides a clearer illustration:

namespace Animal {
    let haveMuscles = true;

    export function animalsHaveMuscles() {
        return haveMuscles;
    }
}

namespace Animal {
    export function doAnimalsHaveMuscles() {
        return haveMuscles;  // Error, because haveMuscles is not accessible here
    }
}
Copy the code

Since haveMuscles is not exported, only the animalsHaveMuscles function that shares the original unmerged namespace can access this variable. The doAnimalsHaveMuscles function is part of the merged namespace, but does not access unexported members.

Namespaces are merged with class and function and enumeration types

Namespaces can be merged with other types of declarations. As long as the definition of the namespace matches the definition of the type to be merged. The merge result contains the declaration types for both. TypeScript uses this functionality to implement some of the design patterns found in JavaScript.

Merge namespaces and classes

This allows us to represent inner classes.

class Album {
    label: Album.AlbumLabel;
}
namespace Album {
    export class AlbumLabel { }
}
Copy the code

The merge rule is the same as in the merge namespace section above. We must export the AlbumLabel class so that the merged class can access it. The result of the merge is a class with an inner class. You can also use namespaces to add static attributes to classes.

In addition to the inner class pattern, it’s not uncommon in JavaScript for you to create a function and then extend it later to add some properties. TypeScript uses declarative merging for this purpose and for type safety.

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"));
Copy the code

Similarly, namespaces can be used to extend enumerations:

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; }}}Copy the code

Illegal merger

TypeScript doesn’t allow all merges. Currently, classes cannot be merged with other classes or variables. To learn how to mimic class merges, see TypeScript mixin.

Module extension

Although JavaScript does not support merging, you can patch imported objects to update them. Let’s look at this toy example:

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

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

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

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

// 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());
Copy the code

Module names are resolved in the same way as module identifiers are resolved with import/export. See Modules for more information. When these declarations are merged in the extension, they are declared as if they were declared in the original location. However, there are two limitations to note:

  1. You cannot declare new top-level declarations in an extension – only declarations that already exist in the module can be extended.
  2. Default exports cannot be extended either, only named exports (because you need to use the name of the export to extend, anddefaultYes reserved keywords – view details# 14080)

The global extension

You can also add declarations to the global scope inside the module.

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

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

Array.prototype.toObservable = function () {
    // ...
}
Copy the code

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

  • \