Static type checking in JS

Originally published May 3, 2019·Tagged #javascript, #tooling, #static-analysis, #react, #web-development

I'm interested in using TypeScript for work. IDE integration with VS Code is fantastic, plus running type-checking at build time prevents a whole category of bugs.

However, we're invested heavily in the Babel ecosystem at this point. In addition, I don't want to add a feature to our codebases if it's going to ruin someone's productivity in the future.

My initial thought was "I want to integrate the TypeScript compiler into our apps." This turned out to be the wrong approach; it would be a ton of work to do so, and I'd likely create maintenance debt along the way. Instead, I listed out my goals:

  1. Run static type checking at compile time.
  2. Get static type hints in my IDE while I'm coding.
  3. Allow incremental adoption. In other words, if I update a project to allow type checking, I should not have to make any code changes all changes should be to the configuration.
  4. I shouldn't have to make changes to the default create-react-app configuration to implement type checking.

First attempt: piggyback on ESLint

I'm already using ESLint for static analysis. There's a plugin for Prettier; why wouldn't there be for TypeScript?

After putting in a fair bit of research, I started to feel like I was hitting a wall. I found typescript-eslint fairly quickly. However, that project requires you to use the @typescript-eslint/parser plugin, which would collide with create-react-app and its usage of babel-parser.

Success: separate build step

My dreams of adding type checking into the existing tooling didn't pan out. I still thought I might be able to "layer" type checking on top of what was already there, without having to swap out the compiler.

This ended up going much more smoothly than I'd anticipated! Once I'd added the right packages, I renamed one of my .jsx files to .tsx, and my IDE immediately rewarded me with some squiggly red underlines!

Packages: npm install --save-dev @types/jest @types/node @types/react @types/react-dom tslint typescript

For tsconfig.json, I mostly accepted the create-react-app defaults, except I set "strict": false since strict checking generally costs me more time than it saves.

// Header: tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve"
  },
  "include": [
    "src"
  ]
}

Lessons learned

Goals are best achieved if they're stated as a list of wants, not a particular plan. If a genie could have added the TypeScript compiler to my project, I'd have wasted a lot of time in interoperability issues in the future. What I really wanted, embodied in the four points listed above, was something simpler, and something that took me less than a day to complete. No genies required. ✨