Type-Decorated JS is 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 drawbacks are: having to do a lot of manual typing (in both senses) for the conversion, and needing an additional build/compilation 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.json, 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:

// jsconfig.json
"compilerOptions": {
"checkJs": true, // protip: this json 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.

However, out of the box the editor only really gives hints - it won’t generate warnings for type-related errors. But once you have tsc installed, you’ll additionally get linter-style warnings 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

In examples like the above, the JSDoc comments allow the editor to infer types for any reference, regardless of how it’s initialized. And note that all of these features work for object and class types, as well as primitives.

End result: you get type hints on all your important code, warnings when you misuse a type - largely the same as you would with Typescript, but without needing a compilation step for your build. And all for the low, low price of writing a few type hints and default argument values!

What a great deal!


Bonus step: Export type declarations and API docs!

There are two other indirect benefits to using Typescript: exporting .d.ts type declaration files (which provide type hints to others using your project as a dependency), and exporting API documentation in formats like HTML or PDF. And vanilla JS with inferrable types can get both of these benefits, for relatively little pain.

But putting these into practice is a bit esoteric, so rather than a step-by-step guide I invite you to check out 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.