How Aliases in TypeScript Save Hours of Your Life
\nHave you ever written something like import UserService from '../../../../../services/UserService'? If yes — you know the pain. Your eyes glaze over, refactoring becomes a nightmare, and any file move breaks the entire import chain. Fortunately, TypeScript (and its configuration file tsconfig.json) offers an elegant solution — paths.
But here's the question: how does it work under the hood? Why, after defining the alias "@/*", can you write import { UserService } from '@/services/UserService'? And most importantly — why does it sometimes not work without additional bundler configuration (webpack, Vite, Rollup)?
In this article, we won't just show you how to configure paths in tsconfig.json. We'll look inside the mechanism, understand path mapping, learn how TypeScript resolves modules, and avoid common mistakes.
\n\nWhat are paths in tsconfig.json?
\nPaths is a section in tsconfig.json that allows you to define custom mappings between import aliases and real file paths. Simply put, you tell TypeScript: "When you encounter an import starting with @utils/, actually look for the file in the src/utils/ folder."
Here's what it looks like in code:
\n\n// tsconfig.json\n{\n "compilerOptions": {\n "baseUrl": ".",\n "paths": {\n "@/*": ["src/*"],\n "@utils/*": ["src/utils/*"],\n "@components/*": ["src/components/*"]\n }\n }\n}\n\n\nNow, instead of terrible relative paths, you write:
\n// Before (nightmare)\nimport { formatDate } from '../../../../utils/dateHelpers';\n\n// After (beautiful)\nimport { formatDate } from '@utils/dateHelpers';\n\n\nYou'll agree, the difference is colossal. But how does TypeScript know where to go?
\n\nHow Module Resolution Works Under the Hood
\nTo understand path mapping in TypeScript, you need to understand the basic mechanism of resolution. When TypeScript compiles your code, it uses a strategy similar to Node.js, but with additional capabilities.
\nThe process looks like this:
\n- \n
- Check for an absolute path (starts with
/) — if yes, search from the file system root. \n - Check for a relative path (starts with
./or../) — resolves relative to the current file. \n - Check for an alias from paths — if the import starts with a prefix defined in
paths, TypeScript replaces it with the corresponding path from the mapping. \n - Search in node_modules — if nothing matches, it looks for the module as an npm package. \n
The key point: TypeScript itself does not change the physical paths in the compiled JavaScript. It just checks types and throws errors if a file is not found. Replacing aliases with real paths at runtime is the job of the bundler (webpack, Vite, tsc-alias).
\n\nWhy Don't paths Work Out of the Box?
\nHere's the most common question on Stack Overflow: "I configured paths, but when I run the application, I get a 'Cannot find module' error." The reason is simple: TypeScript is a transpiler, not a bundler.
\nWhen you run tsc, it generates JS files but leaves aliases untouched. For example, import { foo } from '@utils/bar' will remain in the JS as is. Node.js (or the browser) has no idea what @utils is and throws an error.
To fix this, you need to use one of the following solutions:
\n- \n
- tsc-alias — a utility that replaces aliases with real paths after compilation. \n
- module-alias — an npm package for registering aliases in N