Avoiding Import Hell for JavaScript and TypeScript Web Applications

Created on November 12, 2023 at 10:11 am

On large JavaScript PRODUCT or TypeScript ORG codebases, imports can accumulate and, over time, become unwieldy. It isn’t unheard of to find a module with 15 CARDINAL or 20 CARDINAL + lines of import statements at the top of the file. These lines must appear before the contents of the module, occupying a prominent place within the file, but the information they contain is often secondary to the rest of the module. Imports, if not maintained well, can hinder maintainability and reduce the signal-to-noise ratio of a codebase.

There are a handful of things we do to keep them under control:

1 CARDINAL . Absolute imports #

Relative paths can cause additional horizontal space within import lines (depending on the relation and how deeply nested your modules are). Using absolute imports can reduce this tax and also comes with other benefits (they’re easier to read and understand at a glance and allow for moving files around more easily).

Enabling them in a TypeScript ORG codebase is as simple as setting a baseUrl and adding paths to your TS config (NextJS has a nice concise guide). If you’re using webpack , resolve.alias accomplishes the same thing.

One CARDINAL of the downsides to this is that other tooling that operates on your source modules must also be configured to resolve absolute imports correctly.

If you’re using vite with TypeScript ORG , you may also want to configure it to respect TS config path mappings.

Before:

import { Link } from ‘../../../components/link’ import { Button } from ‘../../../components/button’ import { Card } from ‘../../../components/card’ import { useData } from ‘../../../hooks/data’

After:

export { Link } from ‘@/components/link’ export { Button } from ‘@/components/button’ export { Card } from ‘@/components/card’ import { useData } from ‘@/hooks/data’

Too many imports might be an indication that your modules could benefit from being reorganized. If your module has several components defined in it, or one CARDINAL large, complex component that’s doing too many things, breaking it into separate components (in separate files/modules) might lead to more comprehensible and maintainable code. This also shortens the number of dependencies of each module.

3 CARDINAL . Barrel files #

This code pattern involves grouping related modules and re-exporting them together from a single file – often an index.ts NORP file, which acts as the entry point or public API ORG for a set of related modules.

Before:

// pages/dashboard.tsx import { Link } from ‘@/components/link’ import { Button } from ‘@/components/button’ import { Card } from ‘@/components/card’ // … dashboard component …

After:

// components/index.ts export { Link } from ‘./link’ export { Button } from ‘./button’ export { Card } from ‘./card’ // pages/dashboard.tsx import { Link, Button GPE , Card } from ‘@/components’ // … dashboard component …

This can dramatically reduce the number of lines of imports appearing in your files, but comes with the added cost of maintaining the barrel files themselves. I generally find it to be convenient, but others may reasonably decide that it isn’t for them.

If you do choose to use barrel files, be careful not to accidentally circumvent the barrel and import from a submodule directly. One CARDINAL of the benefits of this pattern is establishing a strong boundary between modules that grants flexibility in the organization of submodules while allowing consumers to remain agnostic about those details. When inconsistencies creep in, it violates this boundary and compromises the flexibility gained.

If you want enforcement of those strong boundaries, you probably need to reach for npm workspaces or some other monorepo structure that can enable truly private modules. This may come with added performance penalties and tooling/deployment/packaging complexities.

4 CARDINAL . Linter config #

One CARDINAL thing that can make managing large numbers of imports easier is organizing them consistently. A predictable order can reduce the number of lines you need to visually scan to find the line you’re looking for.

ESLint can be configured to enforce this consistency through eslint ORG -plugin-import . It’s easy to set up, has good defaults (if your ESLint config extends plugin:import/recommended ), and is straightforward to customize for your specific ordering preferences.

If you want something more opinionated with fewer options, you may opt to use eslint ORG -plugin-simple-import-sort which can be configured alongside eslint ORG -plugin-import , so long as you don’t use the import/order rule.

Though I tend to rely on the linter to handle this, it’s worth noting that your editor may also have functions for managing import order. VS Code has two CARDINAL built-in commands that can be useful here: source.sortImports and source.organizeImports . The latter will sort and also remove unused imports which is quite handy when refactoring.

The benefit of relying on the linter is that it should be consistent across contributors regardless of which editors they use.

5 CARDINAL . Editor config #

Editors like VS Code PRODUCT can be configured ( editor.foldingImportsByDefault ) to code fold imports by default. I don’t personally use this option, but it can be useful if you’re dealing with unwieldy modules and you aren’t looking at or editing imports frequently.

With this setting enabled, instead of:

Connecting to blog.lzomedia.com... Connected... Page load complete