Declaration Merging Flashcards

1
Q

What is “declaration merging”?

A

“declaration merging” means that the compiler merges two separate declarations declared with the same name into a single definition. This merged definition has the features of both of the original declarations.

Any number of declarations can be merged; it’s not limited to just two declarations.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

Explain the 3 declaration groups a declaration can belong to in TypeScript

A
  • namespace: Namespace-creating declarations create a namespace, which contains names that are accessed using a dotted notation.
  • Type: Type-creating declarations do just that: they create a type that is visible with the declared shape and bound to the given name.
  • Value: Value-creating declarations create values that are visible in the output JavaScript.
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

List all the declaration types and the declaration group they belong to.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

Interface merging

A

interface merging the merge mechanically joins the members of both declarations into a single interface with the same name.

interface Box {
  height: number;
  width: number;
}
interface Box {
  scale: number;
}
let box: Box = { height: 5, width: 6, scale: 10 };
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

Limits of interface merging

A

Non-function members of the interfaces should be unique. If they are not unique, they must be of the same type. The compiler will issue an error if the interfaces both declare a non-function member of the same name, but of different types.

For function members, each function member of the same name is treated as describing an overload of the same function. Of note, too, is that in the case of interface A merging with later interface A, the second interface will have a higher precedence than the first.

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 will merge to create a single declaration as so:

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

Notice that the elements of each group maintains the same order, but the groups themselves are merged with later overload sets ordered first.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

What is the exception of specialized signatures on interface merging

A

If a signature has a parameter whose type is a single string literal type (e.g. not a union of string literals), then it will be bubbled toward the top of its merged overload list.

For instance, the following interfaces will merge 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 resulting merged declaration of Document will be the following:

interface Document {
  createElement(tagName: "canvas"): HTMLCanvasElement;
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
  createElement(tagName: string): HTMLElement;
  createElement(tagName: any): Element;
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

namespace merging

A

To merge the namespaces, type definitions from exported interfaces declared in each namespace are themselves merged, forming a single namespace with merged interface definitions inside.

To merge the namespace value, at each declaration site, if a namespace already exists with the given name, it is further extended by taking the existing namespace and adding the exported members of the second namespace to the first.

The declaration merge of Animals in this example:

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

is equivalent to:

namespace Animals {
  export interface Legged {
    numberOfLegs: number;
  }
  export class Zebra {}
  export class Dog {}
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

What happens with non-exported members on namespace merging?

A

Non-exported members are only visible in the original (un-merged) namespace. This means that after merging, merged members that came from other declarations cannot see non-exported members.

We can see this more clearly in this example:

namespace Animal {
  let haveMuscles = true;
  export function animalsHaveMuscles() {
    return haveMuscles;
  }
}
namespace Animal {
  export function doAnimalsHaveMuscles() {
    return haveMuscles; // Error, because haveMuscles is not accessible here
  }
}

Because haveMuscles is not exported, only the animalsHaveMuscles function that shares the same un-merged namespace can see the symbol. The doAnimalsHaveMuscles function, even though it’s part of the merged Animal namespace can not see this un-exported member.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

Benefits of merging namespaces with Classes

A

Gives the user a way of describing inner classes.

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

The visibility rules for merged members is the same as when we merge namespaces, so we must export the AlbumLabel class for the merged class to see it. The end result is a class managed inside of another class. You can also use namespaces to add more static members to an existing class.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

Benefits of merging namespaces with Functions

A

Namespaces can be used to extend Functions with static members:

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"));
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

Benefits of merging namespaces with Enums

A

Namespaces can be used to extend enums with static members:

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;
    }
  }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

Is it possible to merge classes with classes in TypeScript?

A

No, currently, classes can not merge with other classes or with variables. For information on mimicking class merging, see the Mixins in TypeScript.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

Module augmentation

A

Modules can not be merged but they can be augmented with limitations.

In JavaScript you can do create a module in one file and imported in another module and “augment” it.

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

This works fine in TypeScript too, but the compiler doesn’t know about Observable.prototype.map. You can use module augmentation to tell the compiler about it:

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

You are in essence declaring an ambient module to extends the Observable class.

However, there are two limitations to keep in mind:

1.- You can’t declare new top-level declarations in the augmentation — just patches to existing declarations.
2.- Default exports also cannot be augmented, only named exports (since you need to augment an export by its exported name, and default is a reserved word - see #14080 for details)

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

Global augmentation

A

You can also add declarations to the global scope from inside a module:

// observable.ts
export class Observable<T> {
  // ... still no implementation ...
}
declare global {
  interface Array<T> {
    toObservable(): Observable<T>;
  }
}
Array.prototype.toObservable = function () {
  // ...
};

Global augmentations have the same behavior and limits as module augmentations.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly