Migrating to Typescript: Write a declaration file for a third-party NPM module

You have an existing node.js application with a lot of NPM modules. That hasn’t been a problem with vanilla JavaScript. The primary advantage of TypeScript is the static type checker. To take advantage of that, you’ll need to start adding type annotations to your code, including code from third-party npm modules.

TypeScript uses declaration files to understand the types and function signatures of a module. The process to adding these declaration files to your project has changed so information you find online could be describing an out-of-date method.

As of TypeScript 2.2, there is a straight-forward way to add a declaration file for a popular npm module. All you need to do is:

npm install --save-dev @types/module
// for example:
npm install --save lodash
npm install --save-dev @types/lodash

npm will create node_modules/@types with subdirectories for each module with an index.d.ts file. That file doesn’t contain any code. It’s only a file that describes the module’s interface, such as classes and types. You still need to import the actual module.


What about modules that don’t have any declaration files?

Inevitably, you’ll find that you’re using a module that doesn’t have any declaration files in npm. You’ll need to write your own if you want to leverage static type checking.

I spent hours trying to solve this problem. The TypeScript documentation doesn’t explain how to do this and the information I found online was often for older versions of TypeScript. I’m writing this in the hope that at least one person out there will save hours of time trying to figure out how to write a declaration file for a third-party module.

First, let’s look at tsconfig.json. There is a property typeRoots that is not set by default but that configures where to search for declaration files. By default, this searches for node_modules/@types. Since it’s only searching inside node_modules, that’s not a location where you can put your own files.

So, the first step is to add a new directory to our project where we want to store our own declaration files. In this example, I’ll use @types for that directory, but you can name it anything you want.

tsconfig.json
{
"compilerOptions": {
"outDir": "./built",
"allowJs": true,
"noImplicitAny": true,
"strictNullChecks": true,
"target": "es6",
"module": "commonjs",
"typeRoots": [
"@types",
"./@types"
]
},
"include": [
"./src/**/*"
],
"exclude": [
"node_modules"
]
}

This configuration file turns on noImplicitAny which means that you must explicitly add type annotations. You should turn that off if you have a large project that you want to migrate over time.

It also adds typeRoots: ["@types", "./@types"] . This tells the TypeScript compiler to look for .d.ts files in both node_modules/@types as well as our custom directory ./@types. Note that all the original JavaScript source files were moved into src to facilitate TypeScript compiling.

Now, we can create our custom declaration file. For this example, I’ll be showing how to write a declaration file for the npm module dir-obj, because that’s the problem I was trying to solve.


Let’s start by creating a new project

mkdir ~/dev/myproject
cd ~/dev/myproject
mkdir src
mkdir built
vim tsconfig.json
# Add outDir, include, and set noImplicitAny
<paste>
{
"compilerOptions": {
"outDir": "./built",
"module": "commonjs",
"target": "es6",
"noImplicitAny": true,
"sourceMap": false
},
"include": [
"src/**/*"
]
}
</paste>
vim src/index.ts
<paste>
import * as dirObj from 'dir-obj';
const project = dirObj.readDirectory(__dirname + '/..', {
fileTransform: (file: dirObj.File) => {
return file.fullpath;
}
});
console.log(JSON.stringify(project, null, 2));
</paste>

This simple file will read the project directory structure and output the full path to each file.

To compile the TypeScript file into an ES5 JavaScript file, from the project root, run:

tsc -p .

-p tells tsc to look for the tsconfig.json file in the current directory.


Warning! Could not find a declaration file for module

src/index.ts(1,25): error TS7016: Could not find a declaration file for module 'dir-obj'. '/Users/chris/dev/personal/typescript-examples/node_modules/dir-obj/index.js' implicitly has an 'any' type.

In the current setup, tsc cannot static type check that our code is valid. For that, we need to add a declaration file.

mkdir src/@types
mkdir src/@types/dir-obj
vim src/@types/dir-obj/index.d.ts

Here we created our custom @types directory within the src directory so that the files will be automatically included during compilation.

We added a declaration file for the module dir-obj. Your declaration files must be within a directory that matches the name of the npm modules.


Create the declaration file

/// <reference types="node" />

declare module 'dir-obj' {
import { Stats } from "fs";

export interface readOptions {
filter?: RegExp | Filter,
dirTransform?: DirTransform,
fileTransform?: FileTransform
}

export type Filter = (file: File) => boolean;
export type DirTransform = (file: File, value: any) => any;
export type FileTransform = (file: File) => any;

export function readDirectory(dir: string, options?: readOptions): object;

export class File {
key: string;
readonly path: string;
readonly fullpath: string;
readonly ext: string;
readonly name: string;
readonly basename: string;

constructor(dir: string, file: string);

readonly attributes: Stats;
readonly isDirectory: boolean;
readonly isRequirable: boolean;
}
}

We start the declaration file with declare module 'dir-obj' to explicitly state the module that we’re documenting.

Compile the project

tsc -p .

Finally, there are no compilation errors.