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'