Quick Tip - Specific Local Module Declarations

Published

I've been working in a codebase lately that uses a shared module across a few different files - in my case the module is a JSON file that I'm importing and I wanted to enforce consistent types for the value when it was imported across the codebase.

As far as I can tell the recommended solution looked something like this:

1import jsonObject from './some/path/to/the/file.json';
2import type {MyDesiredJSONType} from './some-types';
3
4let actualValue = jsonObject as MyDesiredJSONType;
1import jsonObject from './some/path/to/the/file.json';
2import type {MyDesiredJSONType} from './some-types';
3
4let actualValue = jsonObject as MyDesiredJSONType;

It felt a bit icky to me to not only copy and paste that across the codebase, but also worry about trying to remind others contributing to the code to do the same thing when they want to import the JSON value.

I instead found a neat little alternative approach that works pretty well, using module declarations and import maps (I've written about import maps before in this blog post)!

The trick is to do the following:

  • Create a local <some-filename>.d.ts file (I've opted to call mine local-types.d.ts since I have a few other things in there too)
  • Add the following content to the file:
1declare module "#<some-identifier>" {
2 import type { MyDesiredJSONType } from "./src/types";
3 let jsonObject: MyDesiredJSONType;
4 export default jsonObject;
5}
1declare module "#<some-identifier>" {
2 import type { MyDesiredJSONType } from "./src/types";
3 let jsonObject: MyDesiredJSONType;
4 export default jsonObject;
5}

I recommend prefixing the identifier with a special character, specifically #. This helps to identify it as a local module specifier (as opposed to @ which might map to a scoped 3rd party package), and easily stands out to others reading the codebase!

Finally, and importantly, configure your import maps / paths configs:

  • For Node.js / other node-like tools:
    • Add a new mapping to your package.json imports record
  • For TypeScript:
    • Add a new mapping to your tsconfig.json paths record

Here's an example configuration within a package.json file:

1{
2 "imports": {
3 "#some-identifier": "./src/some-json-file.json"
4 }
5}
1{
2 "imports": {
3 "#some-identifier": "./src/some-json-file.json"
4 }
5}

and here's an example configuration within a tsconfig.json file:

1{
2 "compilerOptions": {
3 "paths": {
4 "#some-identifier": ["./src/some-json-file.json"]
5 }
6 }
7}
1{
2 "compilerOptions": {
3 "paths": {
4 "#some-identifier": ["./src/some-json-file.json"]
5 }
6 }
7}

Note! Just because I'm using a JSON module as an example here doesn't mean this strategy only works with JSON files - you can do this for any kind of file type!

Thats it! This should then let TypeScript use that declared module type whenever you import from this new identifier!