Module Augmentation in TypeScript

Alfred M. Adjei

Before getting into module augmentation, let’s look at some TypeScript merging principles which will become useful as we progress.

In this post we talked about merging interfaces with interfaces. Additionally, we can merge interfaces with classes too. Let’s look at an example.

class Food {
  cheese: string;
}

interface Food {
  bacon: string;
}

const food  = new Food();
food.bacon = "nice bacon";
food.cheese = "sweet cheese";

console.log(food); // {bacon: "nice bacon", cheese: "sweet cheese"}

🐊 Alligator.io recommends

Our recommended TypeScript courses

In our example above, we can see that, the food variable contains both bacon and cheese even though only cheese was declared in the Food class. This is because, the interface was merged with the class.

But what if our interface contains a method, for example, bake

class Food {
  cheese: string;
}

interface Food {
  bacon: string;
  bake(item: string);
}

const food  = new Food();

food.bake("cake"); // Error: food.bake is not a function

Though, the bake method will be shown on the food variable with the help of intelliSense, because the class Food and the interface Food will be merged, calling the bake method will result in an error because interfaces contain only declaration but not implementations. To solve this, we can add the implementation of bake to the Food prototype.

Food.prototype.bake = (item) => console.log(item);

Now calling the bake method will work.

food.bake("cake"); // cake

Enter Module Augmentation

Module augmentation helps us to extend functionalities to third party libraries we may not have access to or classes in other files.

Say we have a Pet class with a name property and feed method.

pet.ts

export class Pet {
  name: string;
  
  feed(feedType: string) {
    console.log(feedType);
  }
}

We then decide to import this class into our index.ts file but instead of using only the methods and properties in the Pet class, we want to add more functionalities. We can do that using module augmentation.

First, we import our Pet class into our index.ts file.

index.ts

import { Pet } from "./pet";

./pet is a module. In order to extend it, we have a declare a module using the same name and in that module, we will declare an interface with the same name as the class we are trying to extend. In the interface, we will include the properties and methods we want to add to the extended class.

index.ts

declare module "./pet" {
  interface Pet {
    age: number;
    walk(location: string);
  }
}

TypeScript will merge both the Pet class and the Pet interface because they can be found in the same ./pet module.

But that’s not all. Remember I explained that, interfaces don’t contain implementation for methods but only their declarations. For that reason, we will add the implementation of the walk method to the prototype of Pet.

Pet.prototype.walk = (location:string) => `Likes to walk in the ${location}`

Now we can call both the methods and properties found in the Pet class and the newly declared Pet interface.

index.ts

const pet = new Pet();

pet.name = "Looty";
pet.age = 3;

pet.feed("bacon"); // bacon
console.log(pet.name = "Looty"); // Looty
console.log(pet.age = 3); // 3
console.log(pet.walk("park")); // Likes to walk in the park

Now you may be wondering, instead of declaring an interface then adding the implementation of walk method to the Pet prototype, why didn’t we just declare a class with the same name so that when the class is initialized, we will have methods from both classes?

The answer is, TypeScript doesn’t allow merging between classes so we can’t create two or more classes with the same name. If you want to merge classes, there is a workaround using TypeScript mixins discussed about in this post or you can use a library I created just for that.

That’s it. Hope this was useful. 😎👌

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#