Kick-start your Typescript migration with JSDoc and ts-migrate

Jeremy Monson
6 min readJan 12, 2021
So you’re telling me I can avoid annoying JS errors?

Goodbye Old Friend

JavaScript is great. Many of us love how quickly and easily we can build applications or prototype new ones. Eventually though, you may reach a breaking point where JavaScript is actually slowing you down. These reasons may include, but are not limited to, the following:

  • Your code-base and engineering team are growing and you need better guard rails and tools to improve collaboration and reduce bugs
  • Your customers/consumers are developers and you want to improve their development experience when using your products by providing type outputs and IntelliSense
  • You want or prefer static typing
  • You want to reduced cognitive load when refactoring code, changing code bases, or context switching
  • You selfishly (or selflessly 😄) want better IDE integration and IntelliSense to help you enjoy coding more and reduce bugs
Migrating birds (source)

Migration Nation

Once you’ve decided you want to use TypeScript you just need to migrate, Easy enough right? Not so fast…

First you might have convince other engineers or teams to help migrate applications, get management on board, build default configurations to be shared, etc…

If you have micro-services you’ll have to ask yourself some additional questions:

  • How do I manage and where do I store my types?
  • Which services or applications do I migrate first?
  • Do I need to version my types?
  • Do I migrate my shared libraries?
  • Do I use shared types packages for my front-ends and back-ends?

You may not have the answers to all these questions, and rightfully so. You shouldn’t let ironing out those important points stop you from preparing yourself for the coming migration.

Here’s where JSDoc and ts-migrate come to the rescue. As stated in the TypeScript docs, it can supply type information to JavaScript files using JSDoc annotations. You can start annotating new code with JSDoc, and adding annotations to old code that you are modifying. Once you’ve come up with a plan for migration and TypeScript support (configuration, packages, testing, etc…) you can use AirBnB’s ts-migrate and it’s JSDoc plugin to automatically migrate JavaScript files to TypeScript with type annotations derived from JSDoc. Pretty cool right?

Using these tools will help you get a head start on your migration and automate some of the annoying (and error-prone) tasks you have ahead of you. I suggest here that you think critically as well about what services or applications you migrate first. If you have any shared code libraries (component libraries, utility function libraries, etc…) I would recommend migrating those first for two reasons:

  • They’re (most likely) version controlled so you can use alpha and beta tags or revert to an older version if something goes wrong
  • They will give you the most bang for your buck since you’ll be able to use the type annotations anywhere the package is consumed
  • The shared libraries will not be an impediment to migration of consuming application and services since they will already have type definitions and you won’t have to manually write them

While you’re working on resolving the items I mentioned earlier, you can start writing JSDoc annotations for new and old code in your shared libraries and prepare them for automated migration with ts-migrate.

Show Me The Money

Okay, you’re sold now so let’s get into an example so that you can get to work. We’ll draft an example of a shared code library using JSDoc annotations to output type definitions into the package that will be uploaded to NPM. I’ll start from the beginning so that everyone can follow along, if you’re just interested in how to output the types you can skip to the end of the example.

First let’s start with creating a new project:

mdkir jsdoc-power-up
cd jsdoc-power-up
npm init // follow the prompts

Next we’ll create an index.js file in the root of the directory will the following contents:

/**
* @typedef {object} SomeComplexObject
* @property {object} a
* @property {string[]} a.b
* @property {object} c
* @property {string | number} c.d
*/
/**
* @param {SomeComplexObject} someComplexObject
* @param {boolean} [someOptionalParam]
* @returns {SomeComplexObject["c"]["d"] | SomeComplexObject["a"]["b"]}
*/
const someSharedFunc = (someComplexObject, someOptionalParam = false) => {
if (someOptionalParam) {
return someComplexObject.c.d
}
return someComplexObject.a.b
}
module.exports = {
someSharedFunc,
}

Okay let’s zoom in a little bit here to figure out what’s going on.

/**
* @typedef {object} SomeComplexObject
* @property {object} a
* @property {string[]} a.b
* @property {object} c
* @property {string | number} c.d
*/

First there is the typedef JSDoc annotation which is equivalent to defining a TypeScript type. This is a reusable JSDoc type definition, which is more readable and reusable than defining complex objects inline with the function definition. We are simply defining the SomeComplexObject type as an object with the properties a and c as object containing their own properties respectively. a.b is defined as an array of string and c.d is defined as either a string or a number.

Below that we have the function definition with it’s JSDoc annotations

/**
* @param {SomeComplexObject} someComplexObject
* @param {boolean} [someOptionalParam]
* @returns {SomeComplexObject["c"]["d"] | SomeComplexObject["a"]["b"]}
*/
const someSharedFunc = (someComplexObject, someOptionalParam = false) => {

He we are adding the type we created above SomeComplexObject to the first parameter of the function someComplexObject and then creating an optional parameter for the boolean flag someOptionalParam . Lastly, we are defining the return type of the function to be either the type of someComplexObject.c.d or someComplexObject.a.b

On top of having the type outputs later, we now also get IntelliSense on our code when editing this function or consuming it. Not only does this make development easier, but auto-fill helps to reduce bugs that are a result of typos or other common human errors.

Getting the type outputs

Now that we have written a simple example, we can generate the type outputs for the shared code library. First we’ll install typescript

npm install --save-dev typescript@latest

Next we will go into our package.json file and add a types property which will let NPM know where to find our type outputs. We’ll also add a script called build:types which will generate our type outputs.

{
"name": "jsdoc-power-up",
"version": "1.0.0",
"description": "Power up your typescript migration.",
"main": "index.js",
"types": "types",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build:types": " tsc --allowJs --declaration --declarationDir types --emitDeclarationOnly index.js",
},
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "4.1.3"
}
}

If you run the command npm run build:types you will now see a new folder in your directory called types with an index.d.ts file that looks like so

export type SomeComplexObject = {
a: {
b: string[];
};
c: {
d: string | number;
};
};
/**
* @typedef {object} SomeComplexObject
* @property {object} a
* @property {string[]} a.b
* @property {object} c
* @property {string | number} c.d
*/
/**
* @param {SomeComplexObject} someComplexObject
* @param {boolean} [someOptionalArg]
* @returns {SomeComplexObject["c"]["d"] | SomeComplexObject["a"]["b"]}
*/
export function someSharedFunc(someComplexObject: SomeComplexObject, someOptionalArg?: boolean): SomeComplexObject["c"]["d"] | SomeComplexObject["a"]["b"];

Wrapping up

Now that you have the type outputs for your package, once you publish it to NPM and install it in one of your application you’ll have all the types defined in order to have IntelliSense and be compatible with TypeScript applications and services. Once you’re ready to fully migrate over to TypeScript, you’ll have a head-start by being able to use ts-migrate to automatically migrate your .js files to .ts and preserve the types that are annotate by JSDoc. For instructions on ts-migrate i’ll refer you to their repository and blog post.

--

--

Jeremy Monson

I’m a software engineer that wants to write words for some reason…