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?
- You are familiar with JSDoc but not ready to learn TypeScript yet.
- You are prototyping or targeting modern browsers only.
- You want advanced IDE support like code completion and highlighting errors.
- You don’t want to use a transpiler, Webpack and friends.
- But want to have all the warm feeling of type safety.
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:
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.