Simplify imports with path mapping

Your TypeScript application uses relative imports. Every time you move a file, the imports break. You’ve configured path mapping, and now things break in an entirely new way.

At first glance, TypeScript’s path mapping feature appears to solve the problem of relative imports. Unfortunately, it’s not quite that simple. Assume we have the following files.

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@components/*": ["src/components/*"]
    }
  }
}

// src/pages/notes/summary.tsx
import Alert from '@components/widgets/alert'

We’re using the mapped path to import the Alert module from the components directory. This works, in so far as it allows TypeScript to locate the Alert module during compilation. Unfortunately, it produces broken JavaScript.

// dist/pags/notes/summary.js
const Alert = require('@components/widgets/alert')

TypeScript transpiles the import statement, but leaves the path as-is. Apparently this is not a bug. The solution is to configure the path mappings for JavaScript as well, using the module-alias package.

Add the following to your package.json. Note the lack of wildcards or trailing slashes; this is important. Also note that we reference the dist directory, not src.

{
  "_moduleAliases": {
    "@components": "./dist"
  }
}

The module-alias package is now configured, but we still need to ensure that it is loaded before any other code. The cleanest solution is to [use node -r]({{< ref “node-dash-r” >}}).

// package.json, for development
{
  "dev-serve": "NODE_ENV=development nodemon -r module-alias/register dist/server.js",
}

// ecosystem.config.js, for production
const apps = [
  {
    name: 'myapp',
    env: { NODE_ENV: 'production', PORT: 5000 },
    script: './dist/server.js',
    node_args: '-r module-alias/register',
  }
]

module.exports = { apps }

Path mapping and Jest

Congratulations, your TypeScript application now compiles and runs. Unfortunately, all of your Jest tests are broken.

The solution to this problem is yet another duplicate path mapping. If you’re beginning to question whether this is worth all the hassle, trust that you’re not alone.

Add the following to your jest.config.js, and try to move on with your life.

module.exports = {
  moduleNameMapper: {
    '@components/(.*)': '<rootDir>/src/components/$1',
  },
}

Sign up for my newsletter

A monthly round-up of blog posts, projects, and internet oddments.