Type Safe JavaScript with JSDoc

Generally developers believe that you need TypeScript or Flow if you want type safety for JavaScript. This article we will explore a third alternative to achieve those same goals using just JSDoc comments and Visual Studio Code as your editor. Technically, we are going to use TypeScript but not for compiling our code. Instead we’ll use it to check the types of our JavaScript code during code time using JSDocs comments and type inference. So there’ll be no need to compile the code to JavaScript. You might have to use Babel, depending on what version of JavaScript you’re using. Or you could just use good old ES5 with this technique. In that case no build would be necessary, other than for minification.

Types are Good

Benefits of Types

  1. Better code analysis
  2. Improved IDE support
  3. Promotes dependable refactoring
  4. Improves code readability
  5. Provides useful IntelliSense while coding

These benefits are provided by using TypeScript, Flow and even JSDoc comments.

Getting Types Right is Hard

Type checkers are inhuman and lack empathy for their users.

Type checking for JavaScript can sometimes be intimidating.

Be Proactive

If you’re new to using a type checker with JavaScript, it’s good to understand how they work. This is a good basic explanation about how Flow checks types.

Don’t assume that because your code passed type checks during build time that there will be no type errors. Type errors that happen during runtime can be avoided by using type guards at appropriate places. In general, type errors are going to be the smallest number of bugs that you will have to deal with. For your users, UI/UX inconsistencies will be the things that drive them up the wall. Management will label UI bugs as Pri-1, and your type errors will at best be Pri-3 or else backlogged.

JSDoc for Types

Unlike with TypeScript, with Babel you can live on the cutting edge using stage 0 - 2 presets with JSDoc. If you use Visual Studio Code, you will get advanced IntelliSense from your JSDoc comments. In fact, the great IntelliSense that TypeScript provides through d.ts files is because these also include JSDoc comments.

Set Up Visual Studio Code

"javascript.implicitProjectConfig.checkJs": true

With plain JavaScript this will give you some basic IntelliSense and flag type errors with red squiggles underneath:

Red squiggles indicate a type problem.

Hovering over the flagged type error will popup an explanation of the problem:

Message about wrong type usage.

Providing JSDoc comments to describe your code’s types will resolve these kinds of type errors.

Alternative Ways to Turn Type Checking On and Off

If your user settings are set to check JavaScript by default, you can opt out for an individual file by putting // @ts-nocheck at the top of the file.

If you’re really having troubling dealing with the types on one line of code or the block of code beginning at that line by putting this right above the line: // @ts-ignore.

If your project has a jsconfig.json file, you can add checkJS to the compiler options to turn on TypeScript checking for JavaScript:

Setting Up Live Type Linting

JSDoc Comments

  1. JSDoc: Main Site
  2. JSDoc: Getting Started
  3. JSDoc: ES6 Classes
  4. Visual Studio Code: JSDoc Syntax Support
  5. Visual Studio Code: IntelliSense based on JSDoc
  6. Visual Studio Code: Supported Features of JSDoc

Defining Types with JSDoc

Indicate a Type:

Possible values for type are number, string, undefined, Array, Object. With arrays, you can optionally use a typed array: any[], or string[], or number[], or Object[].

You can also define union types and intersection types:

Default Types

  1. null
  2. undefined
  3. boolean
  4. number
  5. string
  6. Array or []
  7. Object or {}

You can have a typed array: any[], number[], string[]. You could also have an array of object types: Employee[].

Custom type

Properties

Hovering over person will give us the following type information:

IntelliSense for person object.

Hovering over the name and age properties gives us the following information:

IntelliSense for name property.
IntelliSense for age property.

Proper JSDoc type comments can inform the TypeScript engine precise information about the code, which results in advanced IntelliSense, as illustrated above. It’s not just types, it’s information describing the object and its properties.

Method or Function

And here’s a class constructor with a method:

In the above gist, when we hover over guy.sayName we get the following IntelliSense:

Optional Parameters

Notice how when we type a period after opts we get a popup with possible completions. You can use the up and down arrows keys to scroll through the available types. As you do so, the highlight option will show its type information. In this case, age is highlighted, indicate through the question mark that it is optional. We can see its a property and should be a number. The JSDoc comments made this information available to the text editor’s type engine.

Notice the ? after age. That indicates it’s optional.

Note: The latest version of TypeScript now understands Google Closure compiler syntax for optional parameters/properties. The syntax is different. It uses the = after the type definition. Notice how the = operator is place at the end of the types for age and job. There should be no space.

Expando Properties

Because the new language service is powered by static analysis rather than an execution engine (read this issue for information of the differences), there are a few JavaScript patterns that no longer can be detected. The most common pattern is the “expando” pattern. Currently the language service cannot provide IntelliSense on objects that have properties tacked on after declaration.

The only way to deal with expando properties to to escape them with braces and quotes:

Defining Object Types

Out of the four ways to define an object, the first two are the least useful. They will have the same problem of expando properties that we discused earlier. The third is fine for simple objects. But the fourth approach is the most flexible because you can use it for objects with many different properties. And its more verbose property definitions result in much richer IntelliSense:

And here you see the IntelliSense we get from this:

We could have gone with a simpler approach, which would work for types but would provide less useful IntelliSense:

Although this is type correct, the resulting IntelliSense is very minimal:

Verbosity vs Simplicity

Generics

Type Casting

/** @type{some type here} */(an element to cast)

Typically you have to do type casting when dealing with DOM nodes. The reason for this is simple. TypeScript is a static type checker. Checking occurs as you code and during build time. In contrast, when the code loads in the browser, the browser can do automatic type coercion, from Node to Element or Element to Node. They have different interfaces. You can get a node and invoke Element methods on it. The browser will handle the coercion for you. On the other hand, a type check is going to see that you’re treating one type like another and will flag it as an error. Type casting lets the type checker know that the type should be coerced.

And here’s a gist showing how to do type casting.

Importing Types

Suppose we’re in another file where we’ve imported the createVirtualNode function. We now need to deal with the fact that we are creating virtual nodes and need to show their type. We can import the type definition from another file as follows:

Notice how we first define a custom by importing its definition from the file vnode.js, then we can use it to define the return value for the function.

Batch Imports

import * as MyTypes from '../types'

Use whatever namespace works for your project. Then you can access your types off of that namespace:

/**
* @typedef {MyTypes.Person} Person
* @typedef {MyTypes.User} User
* @typedef {MyTypes.Business} Business
*/

Avoid Type Any

For sure there will be situations, like type casting, where you will have to use type any. You could also just use *. Its the same meaning, just shorter.

Checking Types During Build

Then running npm run checkjs will verify your types. Any errors will be logged in the console along with the line number:

`Type error on line 250 in file lib/vdom.js

We can fix this error with type coercion, letting the type checker know that newVNode.type, which is usually a number, should be treated as a string here:

JSDoc Comments = Type Information++

To be frank, TypeScript’s and Flow’s type systems are much more sophisticated than what JSDoc covers. But then, JavaScript itself is a loosely typed language. Trying to force JavaScript to follow the strictness of TypeScript and Flow semantics can sometimes feel contrived. JSDoc is a more natural fit for the way the language works.

Here’s an example of how Microsoft uses JSDoc comments in their TypeScript definition files to provide better IntelliSense. This is from lib.es5.d.ts:

Type Safety at Runtime

Neither TypeScript, nor Flow nor JSDoc can ensure that there will be no type errors during runtime.

Type checkers only work during code time or build time. What happens during runtime is another story. Browsers have bugs that may surface and affect your code. The libraries and frameworks that you are using will also have undetected bugs. And the web is a hundred thousand times wilder than the Wild West. Smokey the Bear famously said: “Only you can prevent forest fires.” As a deveoper, only you can ensure there are no type errors during runtime by using guards at critical places. Preventing type errors during runtime is particularly difficult when you are using third party libraries and data from diverse sources. Guard your types.

Only you can prevent type errors at runtime!

Whenever you write a function that expects a type, check it before using it.

A non-string value will result in an unexpected render result.

The above function looks fine, but what happens if somehow the value that gets passed in for newTitle is an object or array? We can refactor this function to check for a string:

Check the type first, if it’s wrong, log the problem.

Using conditional logic, you could test for different types and do different things depending on the type. At the end of the day, implementing type guards is the only way to ensure type safety in JavaScript. Using type guards also allows you to output logs or error messages with information to help identify what the problem is.

You can write type safe code that passes linters and type checks but has egregious security vulnerabilities and fatal logic errors. Whether a value is a string or number is going to be the least of your problems.

tsconfig.json

The outDir directive tells TypeScript where to put the d.ts files. The exclude directive tells TypeScript what files/folders to ignore. You always want to ignore your Node modules. You also want to ignore the folder in which this puts the d.ts files: types. I put my tests in a folder named __tests__, so I ignore that. You can change that to whatever you name your folder of tests.

With such a configuration set up, you can simplify the package.json script for type checking to just:

"checkjs": "tsc"

The above features require the latest version of TypeScript, at least 3.7.3, so make sure to update it like this:

npm i -D typescript@latest

Summary

JSDoc provides a compelling solution for types in JavaScript.
  1. You can get the development experience that static type checkers provide by documenting your code with JSDoc comments and using Visual Studio Code with JavaScript type checking enabled. This provides you with most of the features of static typing without requiring a separate build step. In Visual Studio Code, JSDoc comments will enable code completion, showing symbol definitions, symbol renaming across files, flaging of typos and unused variables, incorrect types, code completion, etc.
  2. TypeScript is a compiler that converts the code to JavaScript. Flow is a checker that uses the type information to check the code. A Babel plugin removes the Flow types during build time. Using JSDoc with TypeScript requires no compilation. Instead TypeScript is used to check the code based on the types provided by JSDoc comments. Thus this is more like using Flow.
  3. Dealing with the DOM can be annoying whether using TypeScript or JSDoc for types. Brackets and quotes will be required for expando properties.
  4. If you’re a large team writing JavaScript, by using Visual Studio Code with JSDoc you can ensure that the code being produce has correct types and provides robust IntelliSense to help new developers.
  5. Real type safety means checking your types at runtime. Use type guards where appropriate.
  6. You need to think ahead about how to handle wrong types when they occur. Should you log an error, alert the user, branch the logic to use it, or ignore it altogether?
  7. JSDoc comments provide valuable type information for your code. They are just standard JavaScript comments. That means your code is valid JavaScript ready to run. During minification the JSDoc comments will be automatically removed, including type cast parens and brackets for escaping expando properties. You don’t have to do anything special to remove JSDoc comments from the final minified code.
  8. If you’re a JavaScript developer who would like type safety and IntelliSense for your code but hesitance to go all out for TypeScript or Flow, JSDoc provides a third alternative path with less resistence. Just open up Visual Studio Code, turn on type checking for JavaScript and start adding JSDoc comments to your code.
  9. If you have a background in Java or C#, TypeScript may be a better choice because it’s closer to what you are used to. Nothing wrong with that.
  10. Type problems flagged by TypeScript, Flow or JSDoc will not necessarily generate errors when run in the browser. Although the difference between a string and a number is significant for type checkers, browsers can easily coerce these as needed. Similarly, for code expecting a string, the browser can cast an array of primitive types into a string. However, writing code that depends on JavaScript type coercion can have unexpected results. It’s always better to be explicit and convert values to the type you need.
  11. TypeScript has a number of syntax features not present in JavaScript: interface, public, private, implements. These make JavaScript feel more like Java or C#. Neither TypeScript or Flow can be used in the browser because they mutate the JavaScript to introduce type information. In contrast JSDoc was designed to describe how JavaScript works. It is, after all, a documentation system. It uses standard JavaScript comments to introduce type information and does not mutate the code. JavaScript with JSDoc comments can run in the bowser. JSDoc is for all practical purposes the standard way of commenting JavaScript.

Type Linting for Dummies