typescript: declaration merging

2021-12-22

 | 

~3 min read

 | 

411 words

Declaration merging is a powerful concept in Typescript that allows you to extend type definitions in many ways.

From the Typescript documentation on Declaration Merging

“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.

Basic Declaration Merging

The simplest / most common form of declaration is when you merge two interfaces:

interface Bar {
  baz?: string
}

interface Bar {
  wam?: number
}

const bar: Bar = {
  baz: "abc",
  wam: 1,
  cat: 123, // Error
}

In the above, the two Bar interfaces are merged and bar can readily accept baz and/or wam, though it complains at cat which is not part of the interface.

Module Augmentation

Perhaps more interesting is module augmentation.

As the name implies, module augmentation is when you accept an interface exported by a module, whether that’s locally defined or imported from a third party library, and augment it with your own details.

A common use case for this is when dealing with an API server and you need to extend the general Request / Response types because of middleware (example here).

Using the example from the docs, imagine you have a module observable.ts:

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

Then, in a separate module, map.ts, you want to make use of the Observable, but extend the class with a new prototype method:

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

This works in Javascript, but Typescript will complain as the interface Observable doesn’t have a map method on it.

Using module augmentation, we can fix that:

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

Now, when we want to use it, Typescript knows that the method is available, for example in a consumer.ts module:

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


Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!