Migrating from Javascript to Typescript

Posted: 27 March, 2020 Category: rough notes Tagged: typesripttslinteslint

Prelude

Once, a senior dev force-injected typescript into our unyielding veins. We squirmed a lot and I, for one, was this close to proclaiming it verbose and crapulent twaddle that no codebase could possibly need (least of all ours).

Yet here I am, retrofitting typescript, utterly-avoidably, RETRO-FITTING typescript... into a Javascript project.

'Course, doing things ass-backwardsly from first-principles also happens to be a GREAT way to learn. So do I feel like I learned a lot? To be ENTIRELY HONEST WITH YOU:

No

...Because linting has become an arcane voodoo far beyond the humanly-grokkable rules and at this point I just traipsed through webspace gathering morcels of "try this" and "but have you done...?" and mooshed it all together to get something that effing lints itself, OK? DO NOT JUDGE ME.

Typescipt setup

  1. Get the ts compiler into your IDE setup: e.g. vscode includes typescript support but you still need to install the ts compiler (tsc). yarn add typescript -D.
  2. Add or update the src/tsconfig.json file (which, seems to be json6 and therefore can have comments). Basically, I already had a tsconfig; just had to:

    • throw away the "noEmit": true setting i.e. stop suppressing the output,
    • introduce the outDir setting (where to put all the compiled, output js files)

      {
      "compilerOptions": {
      "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
      "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
      "allowJs": true, /* Allow javascript files to be compiled. */
      "checkJs": true, /* Report errors in .js files. */
      "outDir": "./dist", /* Redirect output structure to the directory. */
      "strict": false, /* Enable all strict type-checking options. */
      "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
      "resolveJsonModule": true /* allows for importing, extracting types from and generating .json files */
      }
      }
  3. Similarly, create/update tests/tsconfig.json as needed / if needed (you can have tsconfigs tucked away in any folder and they sensibly override each other up the chain)
  4. Update package.json and make yourself a nice "lint" script that will run the typescript compiler eg tsc.

Making eslint handle everything

In theory that's it, but at this point, I realised that what I ACTUALLY wanted to do, was to slave ts linting concerns to eslint. So:

  1. Hand over ts linting concerns to eslint because, let's face it, it's been around in this linting game for longer and has a feature set to match. This requires an install of both of

    • @typescript-eslint/eslint-plugin and
    • @typescript-eslint/parser ...config'd thusly in your eslintrc:

      parser: '@typescript-eslint/parser',
      extends: [
      'eslint:recommended',
      'plugin:@typescript-eslint/recommended',
      'plugin:@typescript-eslint/recommended-requiring-type-checking',
      ...
      ...
      ]

      With prettier-related plugins always listed last, if you're using them. Even with careful ordering I found the @typescript-eslint/member-delimiter-style rule kept fighting with prettier over some semi-colons, and I had to override the rule myself. You may run into similar things.

  2. For slaving ts linting to eslint, you will need to tell eslint about your tsconfig, so also add this setting inside the parserOptions block in your .eslintrc file:

    'project': './tsconfig.json'
  3. Now you can make that "lint" script in your package.json a bit beefier: run the typescript compiler to make sure there are no errors, and then run eslint:

    "lint": "tsc --noEmit && eslint \"./src/**/*.{js,ts}\" --fix"
  4. For complaints about not finding the module or whatever whenever you do imports without explicit file extensions, you need:

      settings: {
        "import/resolver": {
          "node": {
            "extensions": [".js", ".ts", ".d.ts"]
          }
        }
      },
  5. Then you need vscode to get out of the way of your carefully config'd linters, so in your .vscode/settings file, have this:

    {
      "editor.codeActionsOnSave": {
        "source.fixAll.eslint": true
      },
      "eslint.validate": [
        "javascript",
        "typescript",
      ],
      "editor.formatOnSave": true,
      "[javascript]": {
        "editor.formatOnSave": false
      },
      "[typescript]": {
        "editor.formatOnSave": false
      },
    }
  6. Then of course once you've converted .js files to .ts files and fixed all your lint issues, you try to run via nodemon, likely relying on babel-node, which will then faceplant over your newly-minted .ts files. So you'll also need to install @babel/preset-typescript, and add preset-typescript to the list of presets in your .babelrc file.

That was pretty much the gist of it. You should be good to go!

Fun fact : a .d.ts contains definitions only (types, interfaces), and as such is a subset of a .ts file, which may contain other things such as functions, classes, etc. emoji-smile