Improve your imports with TypeScript path aliases

💡

This article describes how to configure TypeScript path aliases in React Native projects.

Motivation

Without many words, let’s jump into some examples of usual imports:

import Header from '../../../components/Header';
import ThemeProvider from '../../../../theme/ThemeProvider';
import useLoadData from '../../hooks/useLoadData';

This is what you see and use a lot, right? You probably know the pain of making structural changes. You just moved a file one level deeper in your project structure, and oops… Now you need to add one more ../ in this file to all your imports to fix them.

This is a problem of relative imports – it becomes harder and harder to manage them as the project grows. They also have no sense – to understand them, you need to remember project structure pretty well. And you know what – with relative imports you’ll never learn the project structure well, because usually, you’re just typing ../ and looking into suggestions from your IDE until you find the right one.

Imagine, we can have something like in the snippet below:

import Header from '@components/Header';
import ThemeProvider from '@theme/ThemeProvider';
import useLoadData from '@hooks/useLoadData';

No weird dots and slashes, and imports have much more sense. No refactoring or scale problems as well – imports are absolute, so you can just move your file here and there without any changes in imports. And you can build a great mental model of your project structure, so you don’t even need any help from IDE to import something.

Sounds good, do you agree? Let’s improve our imports with TypeScript path aliases and one babel plugin.

Update TypeScript config

Open your tsconfig.json file. Here we need to add baseUrl and paths properties to compiler options:

"compilerOptions": {
    ...
    "baseUrl": "./src/",
    "paths": {
      "@assets/*": ["assets/*"],
      "@components/*": ["components/*"],
      "@contexts/*": ["contexts/*"],
      "@hooks/*": ["hooks/*"],
      "@modules/*": ["modules/*"],
      "@navigators/*": ["navigators/*"],
      "@screens/*": ["screens/*"],
      "@app-types/*": ["types/*"],
      "@utils/*": ["utils/*"]
    }
}

Different combinations of baseUrl and paths are possible, but you just need to make sure that they form a real absolute path to a file or folder. In the example below baseUrl is ./src/ and it means that all our imports will come from src directory. On the other hand, it means that you cannot use path aliases for imports outside src.

To determine your baseUrl you can follow the closest common parent rule – if all imports are coming from one directory than path to this directory is probably a baseUrl. For example, if all imports are coming from src directory, then you might consider to use ./src/ as your baseUrl. If something is imported outside src (e.g., from assets or public folders) than you may want to use ./(root of the project) as your baseUrl.

You can already use path aliases in your project, and they’ll work perfectly fine in your IDE. The error will happen when you’ll try to launch your app. Our beautiful new imports will not be resolved. To fix this, we need to add a plugin to our babel transpiler that will help to resolve our recently added path aliases.

Install Babel plugin

We are going to use babel-plugin-module-resolver. To install it, run the command:

npm install babel-plugin-module-resolver --save-dev

Or for yarn users:

yarn add -D babel-plugin-module-resolver

Let’s move on to our final step – plugin configuration.

Update Babel config

We need to configure the plugin we just installed and edit babel.config.js file. Open the file and simply copy-paste the code from the snippet below:

const config = require('./tsconfig.json')
 
const { baseUrl, paths } = config.compilerOptions
 
const getAliases = () => {
  return Object.entries(paths).reduce((aliases, alias) => {
    const key = alias[0].replace('/*', '')
    const value = baseUrl + alias[1][0].replace('*', '')
    return {
      ...aliases,
      [key]: value,
    }
  }, {})
}

getAliases function is here to reduce the frustration of adding new aliases – it uses baseUrl and paths properties from our tsconfig.json and constructs a new object in the format required by the plugin. Babel plugin expects to have full absolute paths and without /* at the end:

// The format used in our tsconfig.json
{
  baseUrl: "./src/",
  paths: {
    "@components/*": ["components/*"]
  }
}
 
// The format we need to have in babel plugin config
{
  '@components': './src/components'
}

Then we need to add our babel-plugin-module-resolver config to plugins section:

module.exports = function (api) {
  api.cache(true)
  return {
    // ...
    plugins: [
      [
        'module-resolver',
        {
          extensions: [
            '.js',
            '.jsx',
            '.ts',
            '.tsx',
            '.android.js',
            '.android.tsx',
            '.ios.js',
            '.ios.tsx',
          ],
          alias: getAliases(),
        },
      ],
    ],
  }
}

It’s not working – how to fix

Well… This is the main reason I decided to create this guideline. Configuration usually takes minutes, but then… You can be stuck with a weird unable to resolve module error for hours.

First solution that helps almost always. In one terminal, run the command below:

# react-native start --reset-cache
yarn start --reset-cache

In a new terminal, run yarn ios or yarn android command:

# react-native run-ios
yarn ios
# or
# react-native run-android
yarn android

If you still see the error, try to reinstall node_modules and run watchman watch-del-all command. Repeat the steps above.

Still not working? 🪄 🧙‍️ 🔮 I hope it’ll help you. And feel free to contribute and write down your spells in the comments.