Declaration Merging in TS

Mohammad Jawad (Kasir) Barati - Nov 2 - - Dev Community

I love Typescript and how much it can improve your code quality, though it can sometimes give you a headache if you have not mastered the basics and do not know what a piece of code does and just "copy paste" it from somewhere, and add it to your project that you're working on.

Here I just compiled and put together a very engaging and practical use cases of "declaration merging" in TS. Follow me for more and if you need a Typescript fan who know how TS works.

Here is a link to official doc where I used it as a ref to build this work of art: https://www.typescriptlang.org/docs/handbook/declaration-merging.html


So to being with we know that:

  • In TS we can define the shape of a JS object at the type level.
  • Compiler merges two (or more than two) separate declarations declared with the same name into one.
  • Three groups of declaration entities:
    1. Namespace.
    2. Type.
    3. Value.

We can have a table to show who falls into which category:

Namespace Type Value
namespace
enum
class
interface
function
variable

IMPORTANT:

Knowing what is created with each declaration will help us to understand what is merged when you perform a declaration merge.

So let's look at the most basic example where we add scale to Box interface:

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

let box: Box = { height: 5, width: 6, scale: 10 };
Enter fullscreen mode Exit fullscreen mode

CAUTION:

You most likely also while coding where trying to change type of a property in request object coming from ExpressJS, and then your IDE + TSC was shouting at you that you can NOT do that.

In the example above, in second Box we cannot change type of width to something like boolean. If you do so you'll get:

Subsequent property declarations must have the same type. Property 'width' must be of type 'number', but here has type 'boolean'.ts(2717)

Subsequent property declarations must have the same type.  Property 'width' must be of type 'number', but here has type 'boolean'.ts(2717)

Merging namespace + functions

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"));
Enter fullscreen mode Exit fullscreen mode

This is a well known JS practice of creating a function and then extending the function further by adding properties onto the function. Here we did it in a type safe manner.

Merging namespace + enum

enum Permission { read = 1, write = 2 }

namespace Permission {
  export function hasPermission(
    userPermissions: number,
    permission: Permission
  ): boolean { 
    return (userPermissions & permission) === permission; 
  }

  export function combine(...perms: Permission[]): number {
    return perms.reduce((acc, perm) => acc | perm, 0); 
  }
}
const userPerms = Permission.combine(Permission.read, Permission.write); 
console.log(Permission.hasPermission(userPerms, Permission.read));
Enter fullscreen mode Exit fullscreen mode

Here we are managing user permissions in an application in a very unix-like way. In this way we are utilizing enum for predefined permissions and namespace to encapsulate utility functions. Very readable and easy to understand.

Some tips

  • Mixins: Right now we cannot merge classes with other types, but we can utilize Mixins which are super powerful and I have a great example in my "nestjs-materials" GitHub repo. Go there and search for "PaginationMixin" to see its usage in practice.

GitHub logo kasir-barati / nestjs-materials

NestJS tips, tricks, Notes, Things which are not in doc and I used to figure them out and use them






  • Module augmentation:

    • JavaScript modules do not support merging. Patch existing objects by importing and then updating them.
    • This is the same trick we use for example in ExpressJS to define the shape of req.user.

    See examples for module augmentation in the next slide.

    Patch process.env:

    Patch process.env

    Patch req.user:

    Patch req.user

CAUTION:

  • I usually create a new file called node-env.d.ts or global.d.ts for this.
  • It becomes a module augmentation when we have a top-level import or export statement.

You can also find me on:
Instagram: https://www.instagram.com/node.js.developers.kh/
Facebook: https://www.facebook.com/kasirbarati
X: https://x.com/kasir_barati
YouTube: https://www.youtube.com/@kasir-barati
GitHub: https://github.com/kasir-barati/
Dev.to: https://dev.to/kasir-barati

. . . . . . . . . . . . . . .