Organizing Imports in TypeScript Projects

In this article, I’ll share my experience on how I organize imports in TypeScript projects, the aliasing styles I choose and why, and when I use barrel files.

In modern TypeScript projects, aliases are often used to simplify imports between modules. Instead of writing something like this:

import { Button } from '../../../../../components/Button'

we can use a simpler statement:

import { Button } from '@/components/Button'

This is typically configured using the compilerOptions.paths option in your tsconfig.json file:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Aliases make code more readable and help navigate large projects more easily.

Choosing an Alias Style

I have encountered different alias configurations in various projects. Let’s look at the most popular ones:

~/components/Button

I don’t recommend this style since ~ is commonly associated with the user’s home directory ($HOME) in UNIX-like systems, like mac os or linux. It looks like your component is located at /Users/%username%/components/Button, even though it is within your project.

~components/Button

This is a bit better because it doesn’t conflict with user’s home directory. It’s also may be confused with a directory /Users/componets, since ~username stands for home directory of a user with username in UNIX.

Also, without a trailing slash after ~, you’ll have to configure an alias for each folder separately:

{
  "compilerOptions": {
    "paths": {
      "~components/*": ["./src/components/*"],
      "~utils/*": ["./src/utils/*"],
      "~api/*": ["./src/api/*"]
    }
  }
}

components/Button

This option requires configuring an alias for each folder as well and can lead to conflicts with npm packages. For example:

import { sortMerge } from 'utils/sortMerge'

Is this import from node_modules/utils or src/utils?

@components/Button

This alias has similar issues as the previous one. Using @ without a trailing slash may cause conflicts with namespaces in node_modules:

import { sortMerge } from '@utils/sortMerge'

Is this from src/utils/sortMerge or node_modules/@utils/sortMerge?

@/components/Button 👍

This option avoids the problems mentioned earlier. I recommend it.

With the @ followed by a /, there’s no collision with installed packages in node_modules, and you only need to configure the alias once for all folders:

{
  "compilerOptions": {
    "paths": { "@/*": ["./src/*"] }
  }
}

Interestingly, Next.js projects use this alias style by default, and I consider it a standard choice for TypeScript projects.

prj/components/Button ❤️

When I discussed this with my frontend friends, Andrew Gurylev suggested using the project name or its abbreviation instead of @. For example, if your project is called Urban Innovation, you could write:

import { Button } from 'UI/components/Button'

This requires setting up aliases like this:

{
  "compilerOptions": {
    "paths": { "UI/*": ["./src/*"] }
  }
}

I find this idea appealing when the project name can be shortened concisely.

When Not to Use Aliases

I advise against using aliases to import files within the same “conceptual module”. In such cases, I prefer using relative paths like ./ and ../.

What do I mean by a “conceptual module”? Let’s say you have a MainMenu component with the following file structure:

layout/
  MainMenu/
    MainMenu.tsx
    Item.tsx
    Search.tsx
components/
  Icon/
  Input/
  Link/

Since Item, Toggle, and Search are parts of MainMenu and are only used within its context, I prefer importing them like this:

// in MainMenu.tsx
import { Item } from './Item'
import { Search } from './Search'

However, imports from other modules, such as components, are done via aliases:

// in MainMenu/Item.tsx
import { Icon } from '@/components/Icon'
import { Link } from '@/components/Link'

// in MainMenu/Search.tsx
import { Input } from '@/components/Input'

Why does this matter? For example, during a redesign, you might move the whole component to another directory:

mv src/layout/MainMenu src/old-design/layout/MainMenu

If you imported Item and Search using aliases, you’d have to update the imports:

// in MainMenu/MainMenu.tsx
-import { Item } from '@/layout/MainMenu/Item'
-import { Search } from '@/layout/MainMenu/Search'
+import { Item } from '@/old-design/layout/MainMenu/Item'
+import { Search } from '@/old-design/layout/MainMenu/Search'

Similarly, you’d need to update component imports if they were done without aliases:

// in MainMenu/Item.tsx
-import { Icon } from '../../components/Icon'
-import { Link } from '../../components/Link'
+import { Icon } from '../../../components/Icon'
+import { Link } from '../../../components/Link'

But if you use aliases for external components and relative paths for files within the same widget, your import declarations won’t change at all!

Barrel Files

A “barrel file” is a file that doesn’t contain its own declarations of functions, classes, or variables. It simply re-exports things from other files. Usually, it’s named index.js or index.ts and exports classes, functions, or constants from files within the directory:

// in src/utils/index.ts
export * from './Foo'
export { Bar } from './Bar'
export { Cat, Dog } from './Animals'

This allows us to import everything from that directory using the same path:

// in src/features/Auth.ts
import { Foo, Bar, Cat, Dog } from '@/utils'

Should We Use Barrel Files Everywhere or Avoid Them?

There are mixed opinions on this. Some people place index.ts in every directory:

src/components/index.ts
src/components/Table/index.ts
src/components/Table/Row/index.ts
src/features/index.ts
src/features/users/index.ts
src/features/users/widgets/index.ts
src/features/users/widgets/UserList/index.ts
src/features/users/widgets/UserList/Card/index.ts

As noted in the article Barrel files and why you should STOP using them now, this can have major drawbacks:

  • Slower project builds and test execution due to handling many dependencies.
  • Increased bundle size depending on import/export patterns and how well your bundler handles tree-shaking.
  • Potential cyclic dependencies.

The author suggests a radical approach: avoiding barrel files at all.

Use Only in Conceptual Modules

I prefer a middle ground: using barrel files only for “conceptual modules”. Remember the MainMenu example with this structure?

MainMenu/
  MainMenu.tsx
  Item.tsx
  Search.tsx

Here, we created Item.tsx and Search.tsx to keep MainMenu.tsx from having 1k+ lines of JSX. They are small components split into separate files but are conceptually part of MainMenu and are not used outside it.

So, I create a MainMenu/index.ts file and export only the MainMenu component—this is the public interface of the component’s folder:

// MainMenu/index.ts
export { MainMenu } from './MainMenu'

However, I don’t create a general barrel file for folders like src/components:

// components/index.ts
export * from './Button'
export * from './Input'
export * from './Link'

components is not a reusable module by itself; it’s just an organizational folder.

Conclusions

Do: Set up alias via @/* or project name

import { Button } from '@/components/Button'
import { Icon } from 'prj/components/Icon'

🚫 Don’t: Set up alias via ~/ or @components

import { Button } from '~/components/Button'
import { Icon } from '@components/Icon'

Do: Import files of the same module using relative paths

import { MenuItem } from './MenuItem'

🚫 Don’t: Import files of the same module using aliases

import { MenuItem } from '@/layout/MainMenu/MenuItem'

Do: Use barrel files to export public interface from a “conceptual module”

// src/layout/MainMenu/index.ts
export { MainMenu } from './MainMenu'

🚫 Don’t: Use barrel files to export everything from a module

// src/layout/MainMenu/index.ts
export * from './MainMenu'
export * from './MenuItem' // this is not used outside of MainMenu
export * from './Search'   // this is not used outside of MainMenu

🚫 Don’t: Use barrel files to export from a folder rather than a “conceptual module”

// src/components/index.ts - this is just an "organizing" directory, not a module
export * from './Button'
export * from './Icon'
export * from './Link'