Module Resolution in Large Projects with TypeScript

This post explains how to use custom paths for efficient Module Resolution in large TypeScript projects.

A Primer of Modules

Looking at the State of the Octoverse 2020 you see that TypeScript has surged in popularity. TypeScript adds an unsound type system and powerful compiler to the JavaScript ecosystem.

Architectural overview.

Modules are not exclusive to TypeScript. They have been introduced with ECMAScript 2015 and TypeScript shares this concept.

Modules contain code that is executed within their own scope, not in the global scope. That means all variables, functions, classes, etc. that are declared in a module are not visible outside of the module unless they are explicitly exported using one of the export mechanisms. To consume the exported variable, function, class, interface, etc. it has to be imported using one of the import mechanisms.

The common mechanism today is the ES module: ECMAScript 2015, or ES6 module using the import/export statements.

An example of a module is a [React] (https://reactjs.org/docs/components-and-props.html) component that is shared between different pages. Extracting code into modules does not only make it easier to maintain a large code base and test functionality, but also to optimize your code. ES2015 allows to eliminate unused code via tree shaking.

# DefaultLayout.tsx
import React from 'react'

interface DefaultLayoutProps {
  children: React.ReactNode
}

export const DefaultLayout = ({ children }: DefaultLayoutProps): JSX.Element => (
  <div>{children}</div>
)

export default DefaultLayout

This component has a named export of DefaultLayout and a default export.

Importing Modules

A typical folder structure for the React component πŸ‘† in a Next.js looks like this.

.
β”œβ”€β”€ src
β”‚   β”œβ”€β”€ components
β”‚       └── layout
β”‚           └── DefaultLayout.tsx
β”‚   β”œβ”€β”€ graphql
β”‚   β”œβ”€β”€ hocs
β”‚   β”œβ”€β”€ hooks
β”‚   β”œβ”€β”€ pages
β”‚   β”œβ”€β”€ state
β”‚   β”œβ”€β”€ theme
β”‚   β”œβ”€β”€ types
β”‚   └── utils

To import this DefaultLayout component in the DefaultLayout.tsx the compiler needs to know where the module is located. Usually this is done by specifying a relative path to the component import DefaultLayout from '../../components/DefaultLayout.

However, the TypeScript compiler can be instructed to use a different path to resolve the location of the module. This can be done via the tsconfig.json file.

{
  "compilerOptions": {
   ...
    "paths": {
      "@components/*": [
        "./src/components/*"
      ],
      "@theme/*": [
        "./src/theme/*"
      ],
      "@utils/*": [
        "./src/utils/*"
      ],
      "@hooks/*": [
        "./src/hooks/*"
      ],
      "@state/*": [
        "./src/state/*"
      ],
      "@pages/*": [
        "./src/pages/*"
      ],
      "@hocs/*": [
        "./src/hocs/*"
      ],
      "@type/*": [
        "./src/types/*"
      ],
    }
  }
}

By adding these custom paths for the module resolution, modules the DefaultLayout component can be imported with import DefaultLayout from '@components/layout/DefaultLayout'.

import React from 'react'
import DefaultLayout from '@components/layout/DefaultLayout

const App = (): JSX.Element => <DefaultLayout />
export default App

⚑️ A great resource for writing React apps with TypeScript are the React TypeScript Cheatsheets

Copyright © 2023. All rights reserved 🀘.