Type Checking Vanilla JS with JSDoc and TypeScript

Ever wondered about writing plain old modern JavaScript and still enjoying the benefit of strict type checking? Not quite ready to fully migrate existing projects to TypeScript (or Flow or something else)? The good news is: it’s doable!

A less known feature of the TypeScript compiler is it’s ability use type definitions added to JavaScript by using JSDoc comments.

When is this useful?

Getting started

Enabling TypeScript support on a JavaScript project is straightforward. Add a tsconfig.json file at the project root with the following contents:

{
  "compilerOptions": {
    // Allow checking JavaScript files
    "allowJs": true,
    // I mean, check JavaScript files for real
    "checkJs": true,
    // Do not generate any output files
    "noEmit": true
  }
}

Let’s also add some example code:

greeter.js:

export class Greeter {
  /**
   * @param {string} name
   */
  greet (name) {
    console.log(`Hello ${name}`)
  }
}

main.js:

import { Greeter } from './greeter.js'

const g = new Greeter()
g.greet()

If you open main.js in Visual Studio Code or another editor that has integrated TypeScript support you will see a compile error at g.greet() because we forgot to pass in a required string argument. If you install the TypeScript compiler with npm install --save-dev typescript you can also use the following command to verify your JavaScript code from the command line:

npx tsc

In our case the output will look like this:

$ npx tsc
main.js:4:1 - error TS2554: Expected 1 arguments, but got 0.

4 g.greet()
  ~~~~~~~~~

  greeter.js:5:10
    5   greet (message) {
               ~~~~~~~
    An argument for 'message' was not provided.


Found 1 error.

The compile error can easily be fixed by adding an argument to the function call: g.greet('John').

Using dependencies

Any real project will eventually end up using dependencies from the npm registry. Since all major browsers support native ES6 modules and an increasing number of npm packages are published in this format, it is nowadays possible to develop modularized codebases without any bundling or transpiling tool like Webpack.

It is totally fine to use use a tool, but if you are adventurous like me you can try going all the “vanilla way”. This however needs a bit of further tweaking.

We will use the lodash-es npm module as an example. It is Lodash in the form of many small ES modules.

First install using npm install --save lodash-es. We can import and use the modules directly like this:

import upperFirst from '/node_modules/lodash-es/upperFirst.js'

upperFirst('foo bar')

However the TypeScript compiler does not give us any “protection” here, upperFirst has any type meaning whatever we do, it will compile. We need to install TypeScript definitions separately with npm install --save @types/lodash-es. Once that is done, our tsconfig.json needs to be tweaked a bit:

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "noEmit": true,
    // This resolves types for imports
    // with /node_modules/ prefix
    "baseUrl": ".",
    "paths": {
      "/node_modules/*.js": [
        "node_modules/@types/*"
      ]
    }
  }
}

That’s it. At this point npx tsc should check calls to upperFirst and fail when we make a mistake like passing in an argument of wrong type like upperFirst(42). Visual Studio Code should also pick up the definitions:

Screenshot of Visual Studio Code showing typed upperFirst
function

Summary

The TypeScript compiler allows type checking code written in plain JavaScript and also leverages type definitions created for third party libraries. This not only allows using the compiler as a “very smart linter” but also improves editor and IDE integration (code completion, highlighting errors). This - combined with the power of native ES6 modules - results in lightweight tooling for projects of any size.

One last thing to keep in mind: not all of TypeScript is supported as JSDoc comments and not all of JSDoc syntax is supported by the compiler. Be sure to check out the documentation and understand the differences.