Get full type support with plain JavaScript

Monday, May 15th, 2023, 14:26 #dev

Does this sound familiar to you: you want to write a small script — whether it's for the web, a command line tool, or anything else — and you start in JavaScript... until you remember how painful it is to write code without types. So you rename the file from .js to .ts... and realize you've opened a can of worms.

If you're writing code for a website or a library you introduced the need for a compilation step. If you're building a CLI script you can resort to Deno (which supports TypeScript out of the box) but then you need to setup your IDE to understand the Deno APIs, and it's not always easy to mix and match Deno and node.

Once you got everything working locally, you need to think about how you want to distribute your code. Do you check in your compiled .js files? Do you create a CI pipeline to automatically compile your .ts files? If you're writing a library, how do you publish your library so it's ready to be used by other projects?

You don't actually need TypeScript

The thing is... you don't need to write TypeScript in order to get static type analysis!

You can get all the benefits of TypeScript in JavaScript by using JSDoc

What TypeScript offers is a static type system. That means that the type information has no effect in running code. All the type information is completely lost when your TypeScript is executed (which is the reason why you can't test whether a variable is of a certain type without writing a type guard).

This also means that TypeScript is simply additional type information provided to the TypeScript analyzer without any meaning to the JavaScript engine running your code. When you compile TypeScript to JavaScript, it basically just removes all the type information from your code so it becomes valid JavaScript code again.

JSDoc

Three years after the inception of JavaScript over 25 years ago, JSDoc has been introduced as a way to annotate JavaScript code. It is a formalized markup language that allows IDEs to provide additional context to developers when they see a function.

Similar annotation markup exists in most languages, and I'm sure you already know it. This is what it looks like:

/**
 * This is the JSDOC block. IDEs will show this text when you hover the
 * printName function.
 *
 * @param {string} name
 */
function printName(name) {
  console.log(name)
}

TypeScript and JSDoc

What fewer people know, is that JSDoc is all you need to make full use of TypeScript. The TypeScript analyzer understands types written in JSDoc and gives you the same static analysis it provides to .ts files.

Syntax of types in JSDoc

I won't provide the full documentation of the syntax here. The most important thing is that you know, that nearly everything you can do in .ts files, you can do with JSDoc. But here are a few examples:

Function parameters with native types:

/**
 * @param {string} a
 * @param {number} b
 */
function foo(a, b) {}

Using types that TypeScript provides out of the box:

/**
 * @param {HTMLElement} element
 * @param {Window} window
 */
function foo(element, window) {}

/** @type {number[]} */
let years

Defining object literals and functions:

/** @type {{ name: string; age: number }} */
let person

/** @type {(s: string, b: boolean) => void} */
let myCallback

Import type from *.d.ts files:

/** @param {import('./types').User} user */
const deleteUser = (user) => {}

Define a type for later use:

/**
 * @typedef {object} Color
 * @property {number} chroma
 * @property {number} hue
 */

/** @type {Color[]} */
const colors = [
  { chroma: 0.2, hue: 262 },
  { chroma: 0.2, hue: 28.3 },
]

See the official TypeScript JSDoc documentation for an exhaustive list.

You can still author your *.d.ts files and import them in your JSDoc annotations if you have complex types.

Note that you still need to setup your project (and IDE) for typescript and you need to create a tsconfig.json file with the compiler options allowJs and checkJs set to true:

// tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true
    // ...
  }
}

When to write TypeScript

Although exclusively using JSDoc for your type declarations is possible it is not the most convenient. The TypeScript syntax is just a lot nicer and less repetitive.

The TypeScript team has created a "Types as comments" ECMAScript proposal that would allow you to write TypeScript and run it in a JavaScript engine without modification (JavaScript engines would treat these type annotations as comments.)

But until this proposal is accepted we're stuck with the decision to either use JSDoc or a TypeScript toolchain.

So for now my recommendation is this: When you're working on a project that has a compilation step anyway, there is no downside in using TypeScript. That includes your typical website where you want to optimize your script for production anyway.

But if you don't need a compilation step, then it's probably easier to stick to JSDoc type annotation. Examples of this are libraries and simple scripts.

(Of course there are exceptions to both.)


Thanks for reading. Take a break :)