Type-Decorated JS is the Best JS

I’ve been building and maintaining a large vanilla JS project for quite a while now, so naturally over the years I’ve considered whether I should convert it to Typescript. The advantages are obvious - type safety, better type hints and autocomplete, and more bugs found at dev time instead of runtime. The main drawbacks are having to learn a new language, and having an extra build step.

But over the past 4-5 years, gradually and accidentally, I blundered into a model where I get most of the advantages of Typescript with none of the drawbacks. It boils down to:

  1. Use VSCode or a similarly clever IDE
  2. Install tsc and tell the IDE about it
  3. Write clear vanilla JS such that most types can be inferred
  4. Annote type information with JSDoc comments
    Added bonus: Export type declarations and API docs

That’s the tl;dr. Below are the gory details.


Step 1: Use a good modern IDE

Personally I use VSCode, but everything in this article should be achievable in other modern editors.


Step 2: Add Typescript and a jsconfig.json file

VSCode can do a certain amount of type inference and hinting out of the box, with no additional setup. But for some checks it needs to have tsc (the Typescript compiler) available, so install it from npm.

npm install -g typescript  # installs globally
npm install -D typescript # just for the current folder and subfolders

Most Typescript tutorials will then then tell you how to make tsc compile your project into JS, but we won’t need any of that - as long as it’s available, VSCode should find it.

The next step is to add a jsconfig.json file to your project. It can also be called tsconfig, but since we’re using vanilla JS let’s stay true to the name. The details of jsconfig are here, but if you only care about type hints you can start with something minimal like this:

// jsconfig.json
{
"compilerOptions": {
"checkJs": true // protip: this file can have comments!
}
}


Step 3: Write clear, type-inferrable JS

If you haven’t used vanilla JS recently, you might be surprised at how clever modern IDEs are. Given code like this:

export class Foo {
constructor(name = '', size = 20) {
this.data = { name, size }
}
getData() {
return this.data
}
}

Even when importing that class to a different file, VSCode will give correct type hints out of the box with no setup!

Note that it even knows that foo.getData().name is a string property - it infers this from the default parameter in the class constructor.

Once you have tsc installed, you’ll additionally get linter-style warnings for type-related issues, like below. (Of course you should also use a regular linter - personally I like eslint.)



Step 4: Add more type info with JSDoc comments

You can give the IDE more specific type information with JSDoc-style comments beginning with /**. For example:

/**
* Add a listener
* @param {(name:string) => void} callback
*/
export function addListener(callback) {
// ...
}

Annotated that way, the IDE can now infer all the types you inform it about - so when you pass a callback to the above function, you’ll get type hints and type warnings about the callback’s parameter types, return type, and so on.

You can also use import syntax within comments to invoke types from other files, or the @typdef tag to manually specify types that aren’t declared anywhere locally:

/** @type {import('./foo').Foo} */
var foo = null

/**
* @typedef Bar
* @prop {string} name
*/
/** @type {Bar} */
var bar = null

Be warned however: JSDoc is an old technology, and there several differing specs and implementations floating around. For IDE type hints you can stick to the bare minimum tags, like @type, @typedef, @param, @returns and so on. You can generally ignore more esoteric tags, unless you’re exporting API docs (see below).

End result: you get type hints on all your important code, warnings when you misuse a type, with no need to learn a new language or add a compilation step to your project. All for the low, low cost of writing a few type hints and default argument values!

What a great deal!

…BUT WAIT THERE’S MORE!


Bonus step: Export type declarations and API docs!

A big added bonus to having inferrable types is that you can now, for relatively little pain, export .d.ts type declaration files (which lets people get type hints when using your project as a dependency) and API documentation in a suitable format like HTML or PDF.

Actually putting this into practice is a bit esoteric, so rather than a step-by-step guide let me defer you to how I have it working in my project:

Using that setup, I can then run tsc to export my engine’s type declarations and typedoc to export the engine API reference.