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"
},
"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.

̶I̶t̶ ̶a̶l̶s̶o̶ ̶a̶d̶d̶s̶ ̶t̶y̶p̶e̶R̶o̶o̶t̶s̶:̶ ̶[̶”̶@̶t̶y̶p̶e̶s̶”̶,̶ ̶”̶.̶/̶@̶t̶y̶p̶e̶s̶”̶]̶ ̶.̶ ̶T̶h̶i̶s̶ ̶t̶e̶l̶l̶s̶ ̶t̶h̶e̶ ̶T̶y̶p̶e̶S̶c̶r̶i̶p̶t̶ ̶c̶o̶m̶p̶i̶l̶e̶r̶ ̶t̶o̶ ̶l̶o̶o̶k̶ ̶f̶o̶r̶ ̶.̶d̶.̶t̶s̶ ̶f̶i̶l̶e̶s̶ ̶i̶n̶ ̶b̶o̶t̶h̶ ̶n̶o̶d̶e̶_̶m̶o̶d̶u̶l̶e̶s̶/̶@̶t̶y̶p̶e̶s̶ ̶a̶s̶ ̶w̶e̶l̶l̶ ̶a̶s̶ ̶o̶u̶r̶ ̶c̶u̶s̶t̶o̶m̶ ̶d̶i̶r̶e̶c̶t̶o̶r̶y̶ ̶.̶/̶@̶t̶y̶p̶e̶s̶.̶ Note that all the original JavaScript source files were moved into src to facilitate TypeScript compiling.

UPDATE (2018–02–01): As pointed out in the comments, in recent versions of TypeScript it is no longer necessary to specify typeRoots in the tsconfig.json.

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 will add 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.

The rest of the declaration file is a list of functions and classes that are available in the original JavaScript source, but with the added type information. Figuring out how to look at JavaScript source and figuring out how to write up a type definition for that is out of scope of this article, but hopefully this sets you on the path.

Compile the project

tsc -p .

Finally, there are no compilation errors.


Like what you read? Give Chris Thompson a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.