Check your JS with TS
Tags: javascript, typescript, compile
Published: 2021-01-12 21:43:42

---
title: Check your JS with TS
published: true
date: 2021-01-11 00:00:00 UTC
tags: javascript, typescript, compile
canonical_url: https://whistlr.info/2021/check-js-with-ts/
cover_image: https://storage.googleapis.com/hwhistlr.appspot.com/assets/vscode-warning.png
---

# Check your JS with TS

TypeScript is great and its static analysis and typechecking can help you be more productive, but swapping to TS wholesale isn't possible for many projects. You may also want to keep your project as pure JS, if, like me, you like not compiling when testing in your browser (just serve your files as ESM and let the browser do it).

So, you want check your JS—and this post uses ES Modules, or ESM—with TS tools. Great! This post has three levels of complexity, so read on.

## Basic: Inline types in VSCode

As you hover over symbols in VSCode, you'll see inferred type information: for constants and so on you'll see `string` and `number`. This is the type that TS can safely guess. (If you see `any`, this means that TS can't work out what type you're using.)

You can fill in the gaps here with JSDoc comments which add types. There's a number of ways to specify them:

```js
/** @type {number[]} */
const x = []; // otherwise TS thinks this is 'any[]'

/**
 * @param {Element} bar
 * @param {?Element} barOrNull
 * @return {Promise<void>}
 */
async function fooMethod(bar, barOrNull) {
  // do something with bar/barOrNull
}

/** @type {(arg: number) => void} */
const fn = (arg) => {
  /* ... */
};

// this is a _cast_, not a declaration: you need to wrap in parens ()
const nowIsNumberType = /** @type {number} */ (window['someExternalAny']);
```

Within any `@type {...}`, you can use a combination of [TypeScript's type system](https://www.typescriptlang.org/docs/handbook/basic-types.html) as well as much of [JSDoc](https://github.com/google/closure-compiler/wiki/Types-in-the-Closure-Type-System). The possibilities for types are out of scope of this post.

So—this is fine, but all it gets you is helpful information when you hover over a type or you're trying to autocomplete. Let's get more useful feedback.

## Intermediate: Write TSConfig

If you create a custom "tsconfig.json" file in the root of your project, you can enable warnings and errors for your project. The file should look something like this:

```json
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,

    // if you'd like to warn if you're using modern features, change these
    // both to e.g., "es2017"
    "module": "esnext",
    "target": "esnext",

    // configure as you like: these are my preferred defaults!
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,

    // "strict" implies this, but you'll want to enable it when you're
    // ready: it's a huge reason your project will start complaining
    "noImplicitAny": false,
  },
  "include": [
    // include the JS files you'd like to check here
    "src/**/*.js",
  ],
}
```

You can also use a [base config](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#tsconfig-bases) but you'll still need to specify `include` as well as the top two `compilerOptions` to ensure we just check JS files.

⚠️ Keen observers might also notice that I've included JS-style comments inside the JSON _as well_ as my favourite syntax feature, trailing commas. TypeScript seems to be completely fine with this extended syntax.

### For free: VSCode

Once you create your "tsconfig.json" and make sure it matches your source files, you'll notice something amazing: VSCode will now just start warning you about problems.

![VSCode showing a warning about an unused variable](https://storage.googleapis.com/hwhistlr.appspot.com/assets/vscode-warning.png)<figcaption>VSCode can now show JS warnings, not even just about types</figcaption>

To be clear: I didn't install any TypeScript tooling to make this happen, it was just implicitly part of VSCode. Nice! 🎉

### Command-line: TSC

You can also now run TypeScript via the command-line to get warnings and errors for your whole project, even if it's not compiling your code. Install the NPM package and run its command-line compiler (which will just check, since we set `noEmit` above):

```bash
$ npm install -D typescript
$ tsc
```

If your project has errors—and trust me, for any project where you've not typechecked before, you'll have them—this will print all of them and exit with a non-zero status.

## Advanced: Write/use types

It's all well and good to use types like `number` and `string[]`, but what if you'd like to define your own types—such as a complex interface type with many properties? There's actually many ways to do this in TypeScript, and some background is useful:

- You can [use the triple-slash syntax](https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html) to bring in or reference other types
- You're able to `import` type files, although this only makes sense to TS: not your browser

While the first approach is useful for say, external types—you might depend on something in NPM's `@types` repo or a built-in library—the second is my preferred option for your ESM projects.

### Import your types

If you create a file like "types.d.ts", you can actually import it as "types.js" (and VSCode can suggest this in an autocomplete). TypeScript actually _prevents_ you from importing the ".d.ts" directly-you _must_ pretend that it is a JS file. But the JS file doesn't actually exist—how can this interop with other tools and loading in your browser?

Turns out, we can just create two files: one "types.d.ts" for types, and one "types.js" that's actually just empty. These two files might look like:

```ts
//
// @file types.js
//
// This is an empty file so that browsers and tooling doesn't complain.

//
// @file types.d.ts
//
/**
 * This isn't a real class, it just defines an expected object type.
 */
export interface ArgForSomething {
  foo: string;
  bar?: number;
};

/**
 * We can define functions, too.
 */
export function exportedFunction(arg: ArgForSomething): void;
```

And to use the code, in a regular JS file:

```js
import types from './types.js';

/**
 * @param {types.ArgForSomething} arg
 */
export function foo(arg) {
  // ...
}

/**
 * If you export a function from your types, you can also just reference it
 * wholesale: this might be useful if you're publishing to NPM.
 *
 * @type {types.exportedFunction}
 */
export function exportedFunction(arg) {
  // ...
}
```

Voila—type information!

Importantly, when you're bundling or compiling, tools will hide the dummy, empty file. And during development, the file technically exists, but is ignored since it's empty anyway and is only referenced inside your comments.

### Other approaches

I mention the classic approach for completeness, but this post is really about treating ".d.ts" files as modules. Skip this section unless you're really interested.

So, you can reference other files in your own project using the triple-slash syntax. However, it doesn't mesh well with modules: you can't see anything that had `export` on it in that referenced file, and everything _else_ will be brought into the global namespace. (There's exceptions here too, and it's just more complex than treating it as an ES Module.)

### Export types for others

If you're not publishing to NPM, you can stop reading. But if you are building something that can be consumed further, then read on.

By default, TypeScript looks for the "index.d.ts" file in your project's root directory to provide types for the users of your package. In the example above, I've intentionally _not_ used that name as I think creating an empty peer "index.js" in the top-level of your project is probably going to lead to confusion. I like specifically calling it "types".

You can specify a path to your types in "package.json". (Turns out, TypeScript recommends you do this _anyway_, even if the file is the default name.) This looks like:

```json
{
  "name": "your-awesome-package",
  "types": "path/to/types.d.ts",
  "exports": {
    "import": "./main-module.js",
    "require": "./main-require.cjs"
  }
}
```

This types file should match your top-level export file. It can `import` further ".d.ts" files (and these _don't_ need a dummy peer JS file) and even re-export them.

⚠️ As of writing, TypeScript [doesn't support](https://github.com/microsoft/TypeScript/issues/33079) [subpath exports](https://nodejs.org/api/packages.html#packages_conditional_exports0). There's some workarounds in that thread.

## Summary

Your JavaScript can benefit from TS' static typechecking and errorchecking abilities. It can also reveal a whole bunch of errors or risky behavior you didn't know you had—but hey, that's progress for you. The tools you regularly use—including VSCode, but command-line "tsc" too—are _so_ close to being incredibly useful, even for pure JS, and by giving them the right config, you can get a lot more data.

And of course, while static analysis is great, it's also not a replacement for good tests. Go forth and check your code!