TypeScript module augmentation and handling nested JavaScript files
Learn the basics of module augmentation in TypeScript, and how to add type definitions for nested JavaScript files.
TypeScript has been a valuable addition to the JavaScript ecosystem for over a decade, gaining popularity due to its ability to provide type safety for JavaScript code.
Types for JavaScript packages
For backward compatibility and flexibility, TypeScript allows package authors to provide type definitions for their JavaScript packages. Such type definitions are usually stored in the "DefinitelyTyped" repository. You can use the @types
scope to install these type definitions, for instance, @types/react
is the type definition for the React package.
After installing these type definitions, you can use the package with confidence in type safety. Here's an example of defining a React component with type safety:
Module augmentation
While most popular JavaScript packages have readily available type definitions in the @types
scope, some packages may lack type definitions or have outdated ones. In such cases, TypeScript provides a powerful feature known as "module augmentation" to add type definitions for these packages. Let's take a look at an example:
Now you can use this augmented type definition in your code:
Module augmentation not only allows you to add type definitions for packages but also extends existing type definitions. For example, you can add a new property to the window
object:
And use it in your code:
In a previous post, we have talked about a type issue in the React Router library, and we had to overcome the impact by extending and augmenting the existing type through a custom declaration file.
Remember that when using module augmentation, it's crucial to ensure the accuracy of your type definitions by closely inspecting the JavaScript source code. Incorrect type definitions can lead to runtime errors. In the above example, the window.foo
property must be a string that exists on the window
object.
Global augmentation
Sometimes you may encounter scripts that introduce global variables, and you may want to provide type definitions for these global variables to use them in your TypeScript code. For instance, if you have a script that sets a global variable called __DEV__
:
You can add type definitions for this global variable like so:
Now you can use it in your TypeScript code:
By combining module augmentation and global augmentation, you can even extend type definitions for JavaScript prototypes. However, this is generally not recommended as it can pollute the global scope.
The power of module augmentation allows for such extensions, but exercise caution to prevent global scope pollution.
Nested JavaScript files
In the examples mentioned earlier, we assumed that imports could be resolved through the package's entry file. However, some packages export nested JavaScript files without corresponding type definitions. Consider a package called foo
with the following structure:
The foo
package exports the index.js
file as the entry point and also exports the bar
directory.
To augment the type definitions for the foo
package, you can create a foo.d.ts
file:
However, if you attempt to import the baz.js
file in TypeScript, you'll encounter an error:
To augment the type definitions for the baz.js
file, you need to create a separate baz.d.ts
file:
This ensures that TypeScript can locate the module and its associated type definitions:
To maintain organized type definitions, you can mimic the package's structure in your type definition files:
This approach keeps your TypeScript code structured and type-safe, even when dealing with nested JavaScript files.