Don't use ../.. in imports

or: how to improve the life of your full stop key by 3%!!!!!!

Quite often I see this kind of pattern in imports:

# /pages/about-us.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/home.module.css'

# and then some code...

(this particular example came from the create-next-app template)

Here, we merrily import modules from next (and perhaps other libraries) by referencing them directly (e.g. next/head), but when we import our own module we reference them relative to the current file.

This has two problems:

1: Unintentional coupling

We have created an unintentional coupling between the disk locations of two files:

  • about-us.tsx
  • home.module.css

so for example, if we one day decide to refactor by moving about-us.tsx into a subdirectory (e.g. /pages/marketing/about-us.tsx) then we have to update this import to add another parent directory hop:

import styles from '../../styles/home.module.css'

Coupling isn't always bad -- if two parts of a system rely on each other tightly by sharing a contract, then that's fine as a change in one will naturally effect a change in the other -- but in this case the file move refactoring has caused us to edit an import line which doesn't mean anything useful. It's just busywork.

It also treats our module differently to third-party ones. Although that isn't inherently a crime, how would you feel if we had to write:

import type { NextPage } from '../next'
import Head from '../next/head'
import Image from '../next/image'
import styles from '../styles/home.module.css'

2: It's useless information.

“Programming is the art of telling another human being what one wants the computer to do.”

Donald Knuth

I really like this quote as a guideline for evaluating whether a particular section of code is clean -- what is it telling the reader?

This is best demonstrated by an example where we we have a more nested directory structure, e.g.

# /pages/marketing/worldwide/about-us.tsx

import styles from '../../../styles/home.module.css'

Now what does this import say to the reader?

I want you to go up a level, up another, yup that's good just one more, STOP! Right, now go into the styles directory and pick up a home.module.css thank you very much

Most of that is not useful information, and even though it's trivial it's still a waste of the reader's time and brainpower.

Solution

So how do we avoid this? It's fairly simple, for Typescript just set the baseUrl in tsconfig.json:

{
  "compilerOptions": {
       // ...
      "baseUrl": "./"
  },
  // ...
}

so now our import looks like this:

# /pages/about-us.tsx

import styles from 'styles/home.module.css'

which tell the reader the minimum they need to know, and makes it easy for us to move this file on disk without having to update 1000 lines of imports in future.

If you're not using Typescript you can also set this in jsconfig.json

why are you writing about this

What I've written above isn't particularly revolutionary -- and yet I've seen this crop up in codebases for years and relative imports with mountains of ../../../ still seem to be the default approach!

It's good to have flexible configuration, but I think this should be a default configured out-of-the-box for all new bootstrapped projects, otherwise you'll just end up doing it several months down the line.