Welcome to wechat public account: Front Reading Room
introduce
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.
Declaration Type | Namespace | Type | Value |
---|---|---|---|
Namespace | ❎ | ✅ | ❎ |
Class | ✅ | ❎ | ❎ |
Enum | ✅ | ❎ | ❎ |
Interface | ✅ | ❎ | ✅ |
Type Alias | ✅ | ❎ | ✅ |
Function | ✅ | ✅ | ❎ |
Variable | ✅ | ✅ | ❎ |
Understanding what each declaration creates helps you understand what is merged when the declaration is merged.
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 of a single string literal type (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() {
returnhaveMuscles; }}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") {
returnColor.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.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
}
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 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());
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 had been declared in the original location. However, you cannot declare new top-level declarations in an extension – only declarations that already exist in the module can be extended.
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 {
interfaceArray<T> { toObservable(): Observable<T>; }}Array.prototype.toObservable = function () {
// ...
}
Copy the code
Global extensions have the same behavior and restrictions as module extensions.
Welcome to wechat public account: Front Reading Room