Module Augmentation is a Hidden Gem in TypeScript

In my latest software development project, I encountered a challenge that led me to a hidden gem in TypeScript: module augmentation and interface merging. I was tasked with creating a feature flag package that provided each micro frontend repository with easy, type-safe access to feature flags. We didn’t want the package to have a definitive list of all feature flags. That’s because maintaining package versions would be a nightmare. But, we also didn’t want to allow for a generic string type either. Instead, we needed each micro frontend to define the feature flags it required. Enter module augmentation and interface merging.

Interface Merging

When TypeScript encounters multiple declarations for the same interface, it will merge them into a single interface automatically. This means you can add new properties or methods to an interface in separate declarations, even across different files or modules.

// flags.ts
interface FeatureFlags {
  isBetaFeatureEnabled: boolean;
  isMaintenanceModeEnabled: boolean;
}

interface FeatureFlags {
  isSomeOtherFlagEnabled: boolean;
}

const featureFlags: FeatureFlags = {
  isBetaFeatureEnabled: true,
  isMaintenanceModeEnabled: true,
  isSomeOtherFlagEnabled: false,
};

// Output: true, true, false
console.log(
  featureFlags.isBetaFeatureEnabled,
  featureFlags.isMaintenanceModeEnabled,
  featureFlags.isSomeOtherFlagEnabled
); 

Module Augmentation

Module augmentation is a TypeScript feature that allows you to extend or modify existing modules without altering the source code. This is particularly useful when you need to expand type definitions to fit the unique requirements of your application. Let’s dive into how it works and how you can leverage it for your projects. The general process looks like this:

  1. First, import the module you want to augment.
  2. Next, declare the augmentation using the declare module statement to specify which module you augment. You can add new interfaces, types, methods, or properties in this declaration.
  3. Finally, implement the additional functionality within the module scope.

Feature Flags Demo

To demonstrate module augmentation and interface merging, let’s look at a simple flags package and augmenting the default feature flags using declaration merging.

Inside the flags package, we define a getFlags function that accepts a list of the keys in the Flags interface and returns an object of each of those keys alongside a boolean that indicates if that feature should be enabled or disabled.

// flags.ts
import { Flags } from ".";

type FeatureFlags = {
  [key in T]: boolean;
};

type FlagName = keyof Flags;

export function getFlags<T extends FlagName>(names: T[]): FeatureFlags<T> {
  // Fetch and return your flags here
};

Export the getFlags function and the Flags interface from the flags package with a few defaults that, in this example, all-consuming apps need to incorporate.

// index.ts
export { getFlags } from "./flags";

export interface Flags {
  isBetaFeatureEnabled: boolean;
  isMaintenanceModeEnabled: boolean;
}

Inside consuming apps before setting up module augmentation, if we import and use getFlags we can only retrieve the two flags we set up in the flags package. We now need to augment the Flags interface using module augmentation.

// 🛑 Type '"isSomeOtherFlagEnabled"' is not assignable to type 'keyof Flags'.ts(2322)
getFlags(["isBetaFeatureEnabled", "isMaintenanceModeEnabled", "isSomeOtherFlagEnabled"]);

We can use module augmentation and interface merging to add a new flag to the Flags interface. Augmenting the module ensures that anywhere the Flags interface is used will require the value to exist in the list.

// Assuming the package is called 'flags'
declare module "flags" {
  interface Flags {
    isSomeOtherFlagEnabled: boolean;
  }
}

const flags = getFlags([
  "isBetaFeatureEnabled",
  "isMaintenanceModeEnabled",
  "isSomeOtherFlagEnabled"
]);

Powerful and Flexible TypeScript Features

Module augmentation and interface merging are powerful and flexible features in TypeScript that extend and modify existing modules without altering their original source code. Enhancing existing modules allows libraries to fit specific needs and improves type safety.

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *