TypeScript in JavaScript using JSDoc comments

Describe and check the types of JavaScript | Written by Nathan

Recently I was working on a JavaScript project where I wanted a type checker to help me out, but I didn’t want to make any changes or additions to the project’s current build system. Luckily, TypeScript has a way to provide and check types using JSDoc comments. I’d like to show you how to get started with a few examples below.

I’ve created a repository with all the examples I’m going to reference and scripts to help one get up and running at github.com/shareup/ts-in-js-blog-posts.

(You’ll need to have node installed to work through the examples in this blog post. Checkout the README in the repo for instructions if you need them.)

TypeScript’s compiler (tsc) can be used to check files instead of compiling them. One needs to install tsc.

We can use npx to run tsc. npx will fetch typescript if it’s not yet installed.

$ npx tsc --version
Version 4.0.5

If you have a JavaScript project already, then I recommend installing typescript into the project with npm:

$ npm i --save-dev typescript

Example JavaScript code

The example I’d like to use is a function to average numbers along with a test function to exercise it. I’ll insert a mistake into the test function so we can clearly see when the type checking starts working. Create an average.js file:

export function average (numbers) {
  let sum = numbers.reduce((acc, number) => acc + number)
  return sum / numbers.length
}

export function testAverage () {
  console.debug(average([3, 3, 3, 4]))
  console.debug(average([1, 1, 1, 4]))
  //                      ↓ mistake
  console.debug(average(['a', 1, 1]))
}

Checking types with tsc

By default tsc will read a file.ts and then output a file.js. For this example there are no .ts files so I’ll tell tsc to not “emit” anything when it’s run.

$ npx tsc --noEmit *.js
error TS6504: File 'average.js' is a JavaScript file. Did you mean to enable the 'allowJs' option?

By default TypeScript ignores JavaScript files, so one needs to also specify --allowJs to allow JavaScript to be checked.

$ npm tsc --noEmit --allowJs *.js

Which outputs zero errors. Technically, there aren’t any errors because JavaScript will happily let you add a string and a number together…

JSDoc comments

Let’s make sure TypeScript knows that we only ever intend to average numbers.

The best way to think about the JSDoc comments that TypeScript can read is they specify the types of whatever code is about to be next. So to specify the types of the arguments and return values of a function, we write comments to specify the @params and the @return:

/**
 * @param {number[]} numbers
 * @return {number}
 */
export function average (numbers) {
  let sum = numbers.reduce((acc, number) => acc + number)
  return sum / numbers.length
}

The syntax can take some time to get used to if you are unfamiliar (it did for me). Almost always things are in the order @instruction {type} name with the type always between curly braces. In this instance I’ve written that the argument numbers has the type number[] which can also be written as Array<number> or array of numbers.

All the possible JSDoc annotations TypeScript understands are documented on a single page. I reference this page often.

Does tsc now pickup the mistake where 'a' is provided instead of a number? Well, no.

$ npx tsc --noEmit --allowJs *.js
# …no output…

One not only has to provide the --allowJs flag to tsc, but one also has to add a special comment to the top of every .js file where we intend to have types:

// @ts-check

/**
 * @param {number[]} numbers
 * @return {number}
 */
export function average (numbers) {
  let sum = numbers.reduce((acc, number) => acc + number)
  return sum / numbers.length
}

export function testAverage () {
  console.debug(average([3, 3, 3, 4]))
  console.debug(average([1, 1, 1, 4]))
  console.debug(average(['a', 1, 1]))
}

Now, tsc can notice there is a problem:

$ npx tsc --noEmit --allowJs *.js
average.js:15:26 - error TS2322: Type 'string' is not assignable to type 'number'.

15   console.debug(average(['a', 1, 1]))
                            ~~~

Found 1 error.

Super cool 🎉

Something else interesting to try is to remove the return keyword from the average function and then tsc will complain since we told it @return {number}:

$ npx tsc --noEmit --allowJs *.js
average.js:5:13 - error TS2355: A function whose declared type is neither 'void' nor 'any' must return a value.

5  * @return {number}
              ~~~~~~

Super cool again 🎉

Where to go from here?

This can be very helpful without requiring one to rewrite an entire project in TypeScript. One can opt-in each file so it’s easy to incrementally add types to a project in small steps. And it doesn’t change the final output or bundle of the project at all.

I recommend giving TypeScript in JavaScript using JSDoc comments a try. Checkout the JS Projects Utilizing TypeScript, the big page of all JSDoc annotations supported by TypeScript, and I will definitely write more about this in the future.