DEV Community

Daniel Pereira Volpato
Daniel Pereira Volpato

Posted on

Enabling TypeScript on an existing JavaScript project with custom path mapping (path alias)

Recently, at 3778 healthtech, we wanted to enable TypeScript support on a 3-year old JavaScript backend project and gradually migrate code to TS.

In short, our motivation was mainly to provide type safety (catch type-related errors during compilation/transpilation) for the developers.

This project had some characteristics:

  • it used Babel as transpiler;
  • it had Babel configured to allow path alias (thus avoiding the relative path hell)
  • it used Jest for unit testing.

package.json excerpt:

  "scripts": {
    "start": "node build/index.js",
    "dev": "nodemon --inspect=0.0.0.0:56745 --exec babel-node src/index.js",
    "build": "babel --delete-dir-on-start --copy-files --no-copy-ignored -d ./build src",
    // ...
  }
Enter fullscreen mode Exit fullscreen mode

.babelrc:

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    ["module-resolver", {
      "root": ["./src"]
    }],
    ["@babel/plugin-proposal-decorators", {
            "legacy": true
        }],
    ["@babel/plugin-proposal-optional-chaining"],
    ["@babel/plugin-proposal-class-properties"],
    ["@babel/plugin-proposal-object-rest-spread"],
    ["@babel/plugin-transform-destructuring"],
    ["@babel/plugin-transform-runtime"],
    ["lodash"]
  ]
}
Enter fullscreen mode Exit fullscreen mode

Our Babel config (module-resolver plugin, to be most precise), allowed us to do this kind of import, always relative to ./src folder:

import MyService from "services/MyService";
Enter fullscreen mode Exit fullscreen mode

So, here's how I did it.

Install and setup TypeScript

First, install the needed dev dependencies:

  • typescript: TypeScript itself (version 5.x was just released, so it's the one we chose);
  • @tsconfig/node14: A base tsconfig file for the Node version we use;
  • @types/*: Types for the packages we use so we can take the most out of TS typing feature. For instance, @types/jest, @types/ramda, etc;
npm i -D typescript @tsconfig/node14 @types/jest @types/ramda # other @types 
Enter fullscreen mode Exit fullscreen mode

Then create your tsconfig.json. This is very specific to the current needs of each project, but there are some interesting features for a project where TypeScript and JavaScript source code will coexist for probably a long a while.

Here are the most relevant options:

  • extends: uses the base config we imported earlier
  • baseUrl: sets a base directory from which to resolve non-relative module names. Very useful for path aliasing.
  • allowJs: allow .js files
  • checkJs: whether .js files should be checked or no. In my experience, when migrating a legacy project, you probably want this disabled.
  • exclude: exclude files from compilation, such as node_modules and your test files.
{
  "extends": "@tsconfig/node14/tsconfig.json",
  "include": ["src/**/*"],
  "exclude": ["node_modules", "tests/**/*", "**/*.{spec,test}.{js,ts}"],
  "compilerOptions": {
    "baseUrl": "./src",
    "rootDirs": ["src"],
    "incremental": false,
    "allowJs": true,
    "checkJs": false,
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "skipLibCheck": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Make the compilation work!

To check if your setup is correct, you can invoke tsc, the TypeScript compiler, directly:

npx tsc
Enter fullscreen mode Exit fullscreen mode

Sometimes you may get an error here, depending on your tsconfig.json configuration. This may happen because you don't have any .ts files inside your root dir. In such case, just rename your entry-point file to .ts.

In case of Cannot find module 'xyz' or its corresponding type declarations., you probably need to set the baseUrl property to the correct value, or even use the paths property to map your path aliases.

Then you can adjust your NPM scripts. Edit the scripts in package.json. Here's our setup:

  "scripts": {
    "build": "tsc",
    "build:clean": "rm -rf dist && npm run build",
    "build:watch": "npm run build -- --watch",
  },
Enter fullscreen mode Exit fullscreen mode

Run your app

The compilation is just the first step. We need now to make sure our app can start up properly. So, adjust your start script on package.json:

    "start": "node --unhandled-rejections=strict dist/index.js",
Enter fullscreen mode Exit fullscreen mode

And let's run to see what happens:

npm run build
npm start
Enter fullscreen mode Exit fullscreen mode

Here is my output - it wouldn't be that easy, right ;) :

Error: Cannot find module 'config/vars'
Require stack:
- /home/user/app/dist/index.js
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:902:15)
    ...
(internal/modules/run_main.js:75:12) {
  code: 'MODULE_NOT_FOUND',
    ...
} Uncaught Exception thrown
Enter fullscreen mode Exit fullscreen mode

Depending on your project, you may not have any issues here. In my case, this is happening because, although we instructed tsc to support our path aliases, the generated .js keeps the same paths.

Here is an excerpt of my generated dist/index.js:

// ...
require("config/vars");
// ...
Enter fullscreen mode Exit fullscreen mode

There are some ways to fix this. After looking into packages that can solve this issue, I ended up using tsconfig-paths.

Install it (as a prod dependency, not dev, because we need it at runtime).

npm i tsconfig-paths
Enter fullscreen mode Exit fullscreen mode

And change your start script. According to tsconfig-paths docs, you need to pass environment variable TS_NODE_BASEURL to override baseUrl from tsconfig.json:

package.json:

    "start": "TS_NODE_BASEURL=./dist node --unhandled-rejections=strict -r tsconfig-paths/register dist/index.js",
Enter fullscreen mode Exit fullscreen mode

Run npm start again and success!

Setup your development script

During development, you don't want to build the project every time in order to execute it. So let's configure our dev NPM script.

This time we need a different tool, one that can transpile our code JIT (Just-In-Time). The most used one is ts-node.

Let's install it as dev dependency:

npm i -D ts-node
Enter fullscreen mode Exit fullscreen mode

And setup our package.json:

    "dev": "ts-node -r tsconfig-paths/register ./src/index.ts",
Enter fullscreen mode Exit fullscreen mode

As you can see, we still need to use tsconfig-paths, so we require its register. And we run directly our entry-point .ts file.

Conclusion

I hope this tutorial helped you on adding support for TypeScript with path alias enabled.

In case you have a Dockerfile, remember to adjust it accordingly:

  • COPY tsconfig.json
  • and adjust the entry point CMD.

I intend to do a follow-up on this article showing how to set up Jest and ESLint to work with TypeScript, as well as source-map support and hot-reload for development.

If you like it, please share with your friends, coworkers or someone who may like it.

If you have any doubts, suggestions or corrections, please feel free to contact me.

Top comments (0)