How to get perfect intellisense in JavaScript
Tags: webdev, javascript, productivity, node
Published: 2020-10-20 10:13:18

---
title: How to get perfect intellisense in JavaScript
published: true
description: This article describes how you can use JsDoc annotations in JavaScript to enable a development experience identical to TypeScript
tags: webdev,javascript,productivity,nodejs
cover_image: https://www.blinkingcaret.com/wp-content/uploads/2020/09/jsdoc_type_annotation.png
canonical_url: https://www.blinkingcaret.com/2020/10/07/javascript-intellisense
---

TypeScript is often described as the solution for making large scale JavaScript projects manageable. One of the arguments supporting this claim is that having type information helps catch a lot of mistakes that are easy to make and hard to spot.

Adopting TypeScript might not always be an option, either because you are dealing with an old codebase or even by choice.

Whatever the reason for sticking with plain JavaScript, it is possible to get a nearly identical development experience in terms of having intellisense and development time error highlighting. That is the topic of this blog post.

## VS Code and JavaScript intellisense

If you create a new `index.js` in VS Code and type `conso` followed by `Ctrl+space` (or the Mac equivalent) you'll see something similar to this:

![Intellisense for 'conso' in VS Code](https://www.blinkingcaret.com/wp-content/uploads/2020/09/intellisense_console.png)

The source of the intellisense data is from the type definition files that that are bundled with VS Code, namely console is defined in `[VS Code installation folder]/code/resources/app/extensions/node_modules/typescript/lib/lib.dom.d.ts`. All the files with the `.d.ts` extension in that folder will contribute for what you see in the intellisense dropdown.

TypeScript definition files are one of the sources of intellisense in VS Code.

They are not the only source though. Another source is what VS Code infers from your code.

Here's an example of declaring a variable and assigning it a value. The intellisense is coherent with the type of that value:

![string intellisense example](https://www.blinkingcaret.com/wp-content/uploads/2020/09/string_intellisense.png)

(and yes, you can call `.blink()` or `.bold()` on a string, even in Node.js)

Here's another example where the type is inferred from the usage of a variable in a class definition:

![class where a variable is used as a number and as a string, the inferred type is number or string](https://www.blinkingcaret.com/wp-content/uploads/2020/09/class_intellisense.png)

And additionally to type inference, VS Code will add all the unique words on the file you are editing to the intellisense dropdown:

![intellisense dropdown with methods from a class plus all the unique words in the file](https://www.blinkingcaret.com/wp-content/uploads/2020/09/fall_back_to_words_in_file.png)

Even though the type inference available in VS Code is very clever, it's also very passive.

It won't warn you if you call `myInstance.pethodName()` instead of `myInstance.methodName()`:

![incorrect method name and no error highlighting](https://www.blinkingcaret.com/wp-content/uploads/2020/09/no_error_highlighting.png)

We usually only figure this out at runtime when we get a `TypeError: myInstance.pethodA is not a function`.

Turns out that VS Code has a flag that is turned off by default that when turned on will enable type checking to run through your code, and report errors:

![checkjs highlights that pmethod doesn't exist in myInstance](https://www.blinkingcaret.com/wp-content/uploads/2020/09/check_js_enabled.png)

The flag name is called `checkJs` and the easiest way to enable it is to open "Show all commands" (`Ctrl+Shift+p`) and type "Open workspace settings" and then activate checkJs:

![animation showing the opening of all commands typing open workspace and then clicking checkjs](https://www.blinkingcaret.com/wp-content/uploads/2020/09/enable_check_js.gif)

You might discover that after turning on `checkJs` your file turns into a Christmas Tree of red squiggles. Some of these might be legitimate errors, but sometimes they might not. It doesn't happen often but I've encountered instances where the type definition files for a JavaScript library don't match the latest version (how this happens will become clearer later in the blog post).

If this happens and you are sure that the code you have is correct you can always add at the very top of the file:

    //@ts-nocheck

This will turn off type checking for the whole file. If you just want to ignore a statement you add this immediately before the statement to be ignored:

    //@ts-ignore
    variableThatHoldsANumber = false; //this won't be reported as an error

## Manually providing type information in JavaScript

There are situation where it is impossible for type inference to figure out the type information about a variable.

For example, if you call a REST endpoint and get a list of orders:

    const orders = await getOrdersForClient(clientId);

There's not enough information available for any useful type inference there. The "shape" of what an order looks like depends on what the server that hosts the REST api sends to us.

We can, however, specify what an order looks like using [JsDoc](https://jsdoc.app/) comments, and those will be picked up by VS Code and used to provide intellisense.

Here's how that could look like for the orders:

    /** @type {Array<{id: string, quantity: number, unitPrice: number, description: string}>} */
    const orders = await getOrdersForClient(clientId);

Here's how that looks like in VS Code when you access an order:

![Intellisense sourced from JsDoc annotations](https://www.blinkingcaret.com/wp-content/uploads/2020/09/jsdoc_type_annotation.png)

Even though this can look a little bit cumbersome it's almost as flexible having TypeScript type information. **Also, you can add it just where you need it**. I found that if I'm not familiar with a legacy codebase that has no documentation, adding this type of `JsDoc` annotations can be really helpful in the process of becoming familiar with the codebase.

Here are some examples of what you can do with `JsDoc` type annotations:

### Define a type and use it multiple times

    /**
    * @typedef {object} MyType
    * @property {string} aString
    * @property {number} aNumber
    * @property {Date} aDate
    */

    /** @type {MyType} */
    let foo;
    /** @type {MyType} */
    let bar;

If you use `@typedef` in a file that is a module (for VS Code to assume this there only needs to be an `exports` statement in the file) you can even import the type information from another file.

For example if `@typedef` is in a file named `my-type.js` and you type this from `another-file.js` in the same folder:

    /** @type {import('./my_type').MyType} */
    let baz;

The intellisense for the `baz` variable will be based on `MyType`'s type information.

### Function parameters and return values

Another scenario where type inference can't do much is regarding the parameter types in function definitions. For example:

    function send(type, args, onResponse) {
        //...
    }

There's not much that can be inferred here regarding the parameters `type`, `args` and `onResponse`. It's the same for the return value of the function.

Thankfully there's `JsDoc` constructs that we can use to describe all of those, here's how it would look like if `type` is a `string`, `args` can be anything and `onResponse` is an **optional** function function with two arguments, `error` and `result` and finally the return value is a `Promise` or nothing.

It's a pretty involved example, but it serves to illustrate that there's really no restrictions on the type information we can provide. Here's how that would look like:

    /**
     * You can add a normal comment here and that will show up when calling the function
     * @param {string} type You can add extra info after the params
     * @param {any} args As you type each param you'll see the intellisense updated with this description
     * @param {(error: any, response: any) => void} [onResponse]
     * @returns {Promise<any> | void} You can add extra an description here after returns
     */
    function send(type, args, onResponse) {
        //...
    }

And here it is in action:

![Animated gif that shows how intellisense is displayed as you type the function name and then its parameters](https://www.blinkingcaret.com/wp-content/uploads/2020/09/function_parameters_optimized-2.gif)

### Class and inheritance

One thing that happens often is that you have to create a class that inherits from other classes. Sometimes these classes can even be templeted.

This is very common for example with React where it's useful to have intellisense for the props and state of a class component. Here's how we could do that for a component named `ClickCounter` whose state is a property named `count` which is a number and that also has a component _prop_ named `message` of type string:

    /** @extends {React.Component<{message: string}, {count: number}>}  */
    export class ClickCounter extends React.Component {
        //this @param jsdoc statement is required if you want intellisense
        //in the ctor, to avoid repetition you can always define a @typedef
        //and reuse the type
        /** @param { {message: string} } props */
        constructor(props) {
            super(props);
            this.state = {
                count: 0,
            }
        }

        render() {
            return (
                <div onClick={_ => this.setState({ count: this.state.count + 1 })}>{this.props.message} - {this.state.count} </div>
            );
        }
    }

This is how it looks like when you are using your component:

![Intellisense in JSX](https://www.blinkingcaret.com/wp-content/uploads/2020/09/intellisense_in_jsx.png)

This also possible in function components, for example this function component would have the same intellisense on usage than the class component from the example above:

    /**
    * @param {object} props
    * @param {string} props.message
    */
    export function ClickCounter(props) {
        const [count, setCount] = useState(0);

        return (
            <div onClick={_ => setCount(count + 1)}>{props.message} - {count} </div>
        );
    }

### Casting

Sometimes you might want to force a variable to be of a particular type, for example imagine you have a variable that can be either a number or a string and you have this:

    if (typeof numberOrString === 'string') {
        //there will be intellisense for substring
        const firstTwoLetters = /** @type {string} */ (numberOrString).substring(0, 2);
    }

### Use type information from other modules

Imagine you are writing code in Node.js and you have the following function:

    function doSomethignWithAReadableStream(stream) {
        //...
    }

To enable intellisense for the `stream` parameter as a readable stream we need the type information that is in the stream module. We have to use the import syntax like this:

    /** @param {import('stream').Readable} stream */
    function doSomethindWithAReadableStream(stream) {
        //...
    }

There might be cases though where the module you want to import the type from isn't available out of the box (as stream is). In those cases you can install an npm package with just the type information from [DefinitelyTyped](https://definitelytyped.org/). There's even a [search tool](https://www.typescriptlang.org/dt/search?search=) for looking up the correct package with the typing information you need for a specific npm package.

For example, imagine you wanted typing information for `mocha`'s options, you'd install the type definition package:

    npm install @types/mocha --save-dev

And then you could reference them in `JsDoc` and get intellisense for the options:

![example of intellisense for mocha's options](https://www.blinkingcaret.com/wp-content/uploads/2020/10/mocha_options.png)

## Providing type information to consumers of your module/package

If you were to create a module that exposed functions and classes with the `JsDoc` type annotations that we've been looking at in this blog post, you'd get intellisense for them when that module is consumed from another module.

There's an alternative way of doing this though, with type definition files. Say you have this very simple module using `CommonJS` and this module is defined in a file named `say-hello.js`:

    function sayHello(greeting) {
        console.log(greeting);
    }

    module.exports = {
        sayHello
    }

If you create a file named `say-hello.d.ts` (and place it in the same folder as `say-hello.js`) with this inside:

    export function sayHello(message: string): void;

And you import this function in another module, you'll get the the typing information defined in the `.d.ts` file.

In fact, this is the type of file that the TypeScript compiler generates (along with the `.js` files) when you compile with the `--declaration` flag.

As a small aside, say that you are creating an npm module written totally in JavaScript that you want to share. Also, you haven't included any `JsDoc` type annotations but you still want to provide intellisense.

You can create a type declaration file, usually named `index.d.ts` or `main.d.ts` and update your `package.json` with the `types` (or `typings`) property set to the path to that file:

    {
        "name": "the-package-name",
        "author": "Rui",
        "version": "1.0.0",
        "main": "main.js",
        "types": "index.d.ts"
    }

The type declarations that you put in `index.d.ts` define the intellisense you'll get when you consume the npm package.

The contents of `index.d.ts` don't even have to match the code in the module (in fact that's what the type definition packages in [`DefinitelyTyped`](https://definitelytyped.org/) do).

I'm intentionally leaving the topic of how to write typescript definition files very light here because it's a very dense topic and it's usually easy to find how to provide type information in most cases in the [official docs](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html).

A quick note about TypeScript definition files: a `.d.ts` file does **not** affect the file it "describes", i.e. if you create a type declaration file for module `my-module.js` and in that type declaration file you specify that `functionA` receives a parameter of type `number` and you invoke that function from `functionB` also inside `my-module` you won't get intellisense for `functionA`. Only modules that require/import `my-module` will take advantage of the type information in the type declaration file.

That's it, now think about that large 30+ property configuration object for which you can never remember the exact name of the property you want to set (is it `includeArrayIndex` or `enableArrayIndex` and does it take a `boolean` or a `string`?). Now you don't have to worry about mistyping it and you don't have to look it up everytime.