Introduction

The main selling point of TypeScript is the ability to describe the shapes of JavaSCript objects at the type level. One example that is unique to TypeScript is Declaration Merging.

Declaration Merging

At the simplest level, declaration merging looks like this:

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

interface Box {
    scale: number;
}

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

Here, the Box interface has all three values thanks to the declaration merging provided by TypeScript which merges all declarations for Box into a simpler interface like this:

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

Similar to interfaces, we can also merge modules, namespaces and even classes in TypeScript. The official documentation has a great review of all possible declaration merging scenarios in TypeScript.

Why might you need this?

There can be several scenarios where this might be required. One of the most common ones is when you want to extend an existing JavaScript library that comes with a type definition.

A much less common use case is when you want to extend a module in your own project with another method. Let’s assume you are creating a “greeter” class that works differently based on different scenarios.

// greeter.ts
import CustomerGreeter from './customer_greeter';
import StaffGreeter from './staff_greeter';

export default class Greeter {
  public greet() {
    // …
  }

  public static createGreeter(user: User) {
    if (user.isCustomer()) {
      return new CustomerGreeter();
    } else if (user.isStaff()) {
      return new StaffGreeter();
    }
  }
}

// customer_greeter.ts
import Greeter from './greeter';

export default class CustomerGreeter extends Greeter {
  public greet() {
    // …
  }
}

// staff_greeter.ts
import Greeter from './greeter';

export default class StaffGreeter extends Greeter {
  public greet() {
    // …
  }
}

Everything looks great, right? Let’s build everything (e.g. with webpack) and try to run.

Error: Super expression must either be null or a function

This happens because we have circular references in the project. customer_greeter.ts requires greeter.ts, but greeter.ts requires it back. Webpack can’t decide which one to put before which results in trying to create subclass before the superclass was defined. This can be solved using declaration merging in TypeScript. First, remove the createGreeter and all imports from greeter.ts and then add a new index.ts that extends the greeter module. Since it is not possible to extend the default module in TypeScript, we also need to export Greeter as a named class instead. Therefore, the complete greeter.ts and index.ts now look like this:

// greeter.ts
expor class Greeter {
  public greet() {
    // …
  }
}

export default Greeter

// index.ts
import Greeter from './greeter';
import CustomerGreeter from './customer_greeter';
import StaffGreeter from './staff_greeter';

declare module './greeter' {
  namespace Greeter {
    /**
     * Creates a new Greeter based on the user type
     * @param user User
     * @returns {Greeter} An instance of greeter.
     */
    export function createGreeter(user: User): Greeter;
  }
}

Greeter.createGreeter =  (user: User): Greeter => {
  if (user.isCustomer()) {
    return new CustomerGreeter();
  } else if (user.isStaff()) {
    return new StaffGreeter();
  }
  return new Greeter();
};