Type-safety by augmentation in TypeScript
Leverage TypeScript's module augmentation feature to create type-safe and reusable components in shared libraries while maintaining flexibility for client-specific customizations.
Photo by My name is Yanick under the Unsplash license
Recently, I was tasked with moving a simple Track
component I created to a shared library. The component’s purpose is straightforward: whenever it is rendered inside a React tree, an action is tracked. I designed the component to auto-complete on the available tracking actions whenever I needed to use it:
Moving this component seemed trivial until I realized that I had two options:
- Change the component’s typing to be completely agnostic about the actions that can be tracked (by using
action: string
). - Move the information about the possible actions to be tracked to the library itself, which may cause issues if the library is used by codebases with different tracking actions.
Neither option was ideal.
TypeScript’s Secret Weapon: Module Augmentation
TypeScript interfaces are “open” and allow for extensions. I wondered if I could use this feature to solve my problem: move the Track
component to a shared library with a less strict typing, but make it more strict when used inside a concrete codebase.
It turns out this is possible through module augmentation:
Module augmentation, also known as interface merging, enables you to add new properties, methods, or types to an existing module or interface. When you augment a module, TypeScript merges the augmented definitions with the original ones during compilation.
Applying Module Augmentation
To make it work, I first relaxed the typing of my component and exported an interface that serves as an extension point to restrict the typing from the client:
With that in place, all I need to do is create a special types file (see typeRoots
) in my concrete codebase that takes care of restricting the typings of the Track
component:
By leveraging module augmentation, I can now move the Track component to a shared library while still maintaining type safety and auto-completion for the specific tracking actions in each codebase that uses the library.
Conclusion
TypeScript’s module augmentation feature provides a powerful way to extend and customize the typing of external modules and components. By relaxing the typing in the shared library and providing an extension point through an exported interface, we can achieve type safety and auto-completion tailored to each codebase’s specific needs.
This approach allows for flexibility and reusability of shared components while still maintaining a strong type system. Module augmentation is a valuable tool in a TypeScript developer’s toolkit for creating robust and type-safe code across multiple projects.