
When Absolute Path Imports Don't Work: From Cause to Monorepo Setup
Troubleshooting absolute path import configuration issues in TypeScript/JavaScript projects. Exploring the 'Map vs Taxi Driver' analogy, CommonJS vs ESM history, and Monorepo setup.

Troubleshooting absolute path import configuration issues in TypeScript/JavaScript projects. Exploring the 'Map vs Taxi Driver' analogy, CommonJS vs ESM history, and Monorepo setup.
Clicked a button, but the parent DIV triggered too? Events bubble up like water. Understand Propagation and Delegation.

Stop using 'any'. How to build reusable, type-safe components using Generics. Advanced patterns with extends, keyof, and infer.

Understanding Lexical Scoping. How React Hooks (useState) rely on Closures. Memory management and common pitfalls.

Fixing the crash caused by props coming through as undefined from the parent component.

When I first started learning programming and building toy projects, I didn't worry much about import paths. The file structure was simple, so everything was just one or two folders deep. However, as I started cloning real-world scale projects to study on my own, things got messy.
As I separated components for reusability, gathered utility functions, and managed hooks independently, my folder structure became deeper and deeper. Eventually, the top of my code files looked like a festival of ../../...
import { Button } from '../../../components/ui/Button';
import { useAuth } from '../../../../hooks/useAuth';
import { formatDate } from '../../../utils/date';
import { UserType } from '../../../../types/user';
Looking at this, I thought, "This isn't right." If I moved a file to a different folder, I had to manually update all those paths. Refactoring became pure pain. When I caught myself counting whether there were three or four ../../s, I was convinced there had to be a better way.
That's when I found out about 'Absolute Paths' or 'Path Aliases'. seeing how clean @/components/Button looked, I decided to apply it immediately.
Most search results just said, "Add paths configuration to tsconfig.json." Thinking, "Oh, simpler than I thought?", I applied it right away.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
After saving and checking VSCode, the red squiggly lines disappeared, and autocomplete worked beautifully. Thrilled to escape the ../../.. hell, I modified my code and saved it.
Error: Cannot find module '@/components/ui/Button'
Require stack:
- /Users/me/project/src/pages/index.js
I couldn't understand this situation at all. "Wait, it works fine in VSCode! Cmd+Click takes me to the file perfectly. Why does it claim not to know it only when running?"
My misconception was this: "Use TypeScript, they said. It will automatically transform paths for JS, they said."
But the reality was different. While the editor was happy, the actual runtime environment (Node.js or Browser) had no idea where @/ was pointing to. It was like typing "My Home" into a GPS navigation system, and the system asking back, "Where is that?"
After struggling for days, all my questions were answered when I realized that the roles of 'TypeScript' and 'Runtime (or Bundler)' are completely separate.
Comparing this to a "Map" and a "Taxi Driver" made it click instantly.
TypeScript (The Map):
paths in tsconfig.json is like putting a sticky note on the map saying, "When I say 'My Home', I mean 'Gangnam-gu, Seoul...'".@/components is and doesn't show errors.Runtime/Bundler (The Taxi Driver):
graph LR
subgraph "Design Time (VSCode)"
A[Source Code] --> B[TypeScript Compiler]
B --> C{tsconfig.json}
C -- Checks paths config --> B
B -- OK --> D[Type Check Pass]
end
subgraph "Runtime / Build Time"
E[Bundler / Node.js] --> F{Config File}
F -- Checks alias config --> E
E --> G[File Resolution]
G -- OK --> H[Execution / Bundle]
end
style C fill:#f9f,stroke:#333
style F fill:#9f9,stroke:#333
Why is this so complicated? Blame the history of JavaScript.
For a long time, JS didn't have modules. Then Node.js came with require() (CommonJS/CJS).
Later, browsers adopted import (ES Modules/ESM).
As these two worlds collided, "Module Resolution Strategy" became a mess. TypeScript has to support both, leading to complex configurations to tell TS which environment it's compiling for.
Here, Vite is the "Taxi Driver". You need to add configuration to vite.config.ts.
The recommended way is using vite-tsconfig-paths plugin.
// vite.config.ts
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths()], // Just this one line!
});
This makes Vite automatically read paths settings from tsconfig.json.
Next.js is a really smart driver. If it's configured in tsconfig.json, Next.js's internal Webpack config automatically reads and applies it. So in Next.js, you only need to touch tsconfig.json.
This is where I struggled the most. Node.js doesn't have a browser bundler by default. When you compile TypeScript with tsc, JS files are generated, but the import ... from '@/...' code is converted to JS as is.
The cleanest solution is using tsc-alias. It replaces @/ paths with relative paths (../../) in the compiled JS files.
// package.json scripts
"build": "tsc && tsc-alias"
In real-world scenarios, you might use a Monorepo. For example, if you want to import a button from packages/ui into apps/web.
// apps/web/src/App.tsx
import { Button } from '@repo/ui/Button'; // usage
You need to align the root tsconfig.json with each package's package.json.
The exports field in package.json is crucial because the "Driver" uses it as the entry point.
// packages/ui/package.json
{
"name": "@repo/ui",
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
}
}
Files seem configured, but red lines persist?
tsconfig.json changes immediately. Cmd + Shift + P -> TypeScript: Restart TS Server.paths configuration must go with baseUrl. Ensure baseUrl: "." is present..storybook/main.js or jest.config.js separately.When working in a team, you need rules.
@: Don't mix ~, #. Standardize on @/.@/components/common/Button is fine. @/components/pages/home/sections/features/Item is too deep. Keep it flat.import { Button } from '@/components/Button/Button', use index.ts to enable import { Button } from '@/components'."If you only mark it on the Map (TypeScript) without entering the address in the Navigation (Runtime), the Taxi Driver cannot find the way."
Recall this sentence when absolute path imports don't work. Remember that configuration for the Editor (TS) and configuration for the Executor (Runtime/Bundler) must be paired, and you will no longer fear the Cannot find module error.