Question
TypeScript tsconfig Paths: How Path Mapping Works with Webpack
Question
I am learning about path mapping in tsconfig.json, and I want to use it to avoid long relative imports such as:
import { Something } from "../../../../../lib/src/[browser/server/universal]/...";
My project structure is unusual because it is a monorepo containing multiple projects and libraries. The projects are grouped by company and by target such as browser, server, and universal.
How can I configure tsconfig.json so that I can write imports like this instead?
import { Something } from "lib/src/[browser/server/universal]/...";
Also, is tsconfig.json enough by itself, or do I also need to update my Webpack configuration?
Short Answer
By the end of this page, you will understand how TypeScript path mapping works, how to configure baseUrl and paths in tsconfig.json, and why tools like Webpack usually need matching alias configuration. You will also learn the difference between helping the TypeScript compiler find files and helping your runtime or bundler resolve imports correctly.
Concept
TypeScript supports path mapping through the compilerOptions.paths setting in tsconfig.json. This feature lets you replace long relative import paths with shorter, clearer aliases.
For example, instead of this:
import { Something } from "../../../../../lib/src/browser/utils/something";
You can write something like this:
import { Something } from "@lib/browser/utils/something";
To make that work, TypeScript usually needs two settings:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@lib/*": ["lib/src/*"]
}
}
}
Why this matters
Path aliases improve code in several ways:
- They make imports easier to read.
- They reduce fragile
../../../../chains. - They make refactoring folders easier.
- They help organize large monorepos.
Important limitation
tsconfig.json path mapping is primarily used by TypeScript for type checking and module resolution during compilation. It does not automatically change how Node.js, Webpack, Vite, Jest, or other tools resolve imports.
That means:
- TypeScript may compile successfully.
- But your bundler or runtime may still fail to find the module.
So in many real projects, you must configure the same alias in:
- Webpack
- Jest
- Node runtime tools
- ESLint import resolver
- Other build tools
The key idea
paths teaches the TypeScript compiler what an import string means.
It does not guarantee that every other tool in your project understands the same import string.
Mental Model
Think of import paths like addresses.
A relative import such as:
../../../../../lib/src/browser/tool
is like giving turn-by-turn directions from your current location.
A path alias such as:
@lib/browser/tool
is like giving a saved place name such as "Office".
TypeScript can be taught that "Office" means lib/src. But if Webpack or Node has never heard of that nickname, it will not know where to go.
So:
baseUrlandpaths= teaching TypeScript the nickname- Webpack alias = teaching the bundler the same nickname
- Runtime support = teaching the running app the same nickname if needed
Syntax and Examples
The basic tsconfig.json setup looks like this:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"lib/*": ["lib/src/*"]
}
}
}
What each part means
baseUrl: the starting directory for non-relative module namespaths: custom alias ruleslib/*: the alias pattern used in importslib/src/*: the actual folder TypeScript should look in
Example import
With this config:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"lib/*": ["lib/src/*"]
}
}
}
You can write:
import { Something } from "lib/browser/utils/something";
And TypeScript will resolve it to:
lib/src/browser/utils/something
Better alias naming
Although your example uses lib/..., many teams prefer aliases with a prefix such as @lib/... to make it obvious that the import is an alias and not an installed package.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@lib/*": ["lib/src/*"],
"@browser/*": ["lib/src/browser/*"],
"@server/*": ["lib/src/server/*"],
"@universal/*": ["lib/src/universal/*"]
}
}
}
Usage:
import { Something } from "@browser/utils/something";
import { ServerThing } from "@server/api/serverThing";
Matching Webpack configuration
If you use Webpack, you typically also need aliases there:
const path = require('path');
module.exports = {
resolve: {
alias: {
'@lib': path.resolve(__dirname, 'lib/src'),
'@browser': path.resolve(__dirname, 'lib/src/browser'),
'@server': path.resolve(__dirname, 'lib/src/server'),
'@universal': path.resolve(__dirname, 'lib/src/universal')
},
extensions: ['.ts', '.tsx', '.js']
}
};
This allows Webpack to resolve the same import strings that TypeScript understands.
Step by Step Execution
Consider this tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@lib/*": ["lib/src/*"]
}
}
}
And this import:
import { formatDate } from "@lib/utils/date";
Step by step
-
TypeScript sees a non-relative import:
@lib/utils/date. -
It checks
baseUrlandpathsintsconfig.json. -
It finds a matching pattern:
- alias pattern:
@lib/* - actual target:
lib/src/*
- alias pattern:
-
TypeScript replaces
*withutils/date. -
The resolved path becomes:
lib/src/utils/date
- TypeScript then looks for matching files such as:
lib/src/utils/date.ts
lib/src/utils/date.tsx
lib/src/utils/date.d.ts
lib/src/utils/date/index.ts
- If the file exists, type checking continues normally.
- Later, Webpack must also resolve
@lib/utils/date. If Webpack does not know the alias, the build will fail even though TypeScript understood it.
What this teaches
TypeScript resolution and bundler resolution are related, but they are not the same system.
Real World Use Cases
Path mapping is especially useful in large or multi-package projects.
Monorepos
A monorepo may contain:
- shared UI libraries
- server utilities
- browser-only helpers
- universal modules used everywhere
Aliases help keep imports readable:
import { Button } from "@lib/ui/Button";
import { logger } from "@server/logging/logger";
Frontend applications
Instead of:
import { formatPrice } from "../../../../shared/utils/formatPrice";
You can use:
import { formatPrice } from "@shared/utils/formatPrice";
Backend services
In a Node or API project, aliases make service-layer imports cleaner:
import { validateUser } from "@core/validation/user";
import { db } from "@infra/database";
Shared domain code
When browser and server code share common business logic, a @universal alias makes boundaries clearer:
import { calculateTax } from "@universal/tax/calculateTax";
Large refactors
If folders move around, you may only need to update alias mappings instead of rewriting many relative imports.
Real Codebase Usage
In real projects, developers rarely use paths alone. They usually build a consistent module resolution strategy across tools.
Common patterns
1. Feature-based aliases
Instead of aliasing one huge root folder, teams often define aliases by responsibility:
{
"paths": {
"@components/*": ["src/components/*"],
"@services/*": ["src/services/*"],
"@utils/*": ["src/utils/*"]
}
}
This makes architecture more obvious in imports.
2. Shared aliases across apps in a monorepo
A root tsconfig.base.json may define aliases once, and child projects extend it:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@shared/*": ["packages/shared/src/*"]
}
}
}
Then app-specific tsconfig.json files use:
{
"extends": "../../tsconfig.base.json"
}
3. Keeping bundler config in sync
A common problem is that TypeScript works but Webpack fails. Teams solve this by:
- manually duplicating aliases in Webpack
- using plugins that read
tsconfig.json - standardizing on one alias naming scheme
4. Clear architectural boundaries
Aliases can communicate intent:
@server/*for server-only code@browser/*for browser-only code@shared/*or@universal/*for code safe in both environments
That helps prevent accidental cross-environment imports.
5. Validation and tooling support
In production codebases, aliases are often also configured in:
- Jest
moduleNameMapper - ESLint import resolver settings
- test runners
- editor settings through TypeScript support
Common Mistakes
1. Forgetting baseUrl
Many paths setups require baseUrl.
Broken example:
{
"compilerOptions": {
"paths": {
"@lib/*": ["lib/src/*"]
}
}
}
Safer setup:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@lib/*": ["lib/src/*"]
}
}
}
2. Expecting tsconfig.json to configure Webpack automatically
This is one of the most common misunderstandings.
TypeScript may resolve this:
import { x } from "@lib/x";
But Webpack may throw a module-not-found error unless its resolve.alias matches.
3. Using an alias name that looks like an npm package
If you write imports like:
import { x } from "lib/utils";
some tools may treat lib like a package name. A prefix such as @lib is often clearer.
4. Mismatched wildcard patterns
Broken config:
{
"paths": {
"@lib/*": ["lib/src"]
}
}
This does not map the wildcard correctly.
Correct version:
{
"paths": {
"@lib/*": ["lib/src/*"]
}
}
5. Aliasing directly into unstable internal folders
If you map imports to deep internal folder structures, future refactors can still hurt. Try to alias logical boundaries instead of every physical folder.
6. Mixing relative and alias imports inconsistently
If half the codebase uses relative imports and half uses aliases for the same target, navigation becomes confusing. Teams usually define a rule for when aliases should be used.
7. Assuming runtime support exists everywhere
Node.js does not automatically understand TypeScript path aliases in plain emitted JavaScript. If you run compiled files directly, you may need extra runtime or build-tool support depending on your setup.
Comparisons
| Approach | Example | Best for | Pros | Cons |
|---|---|---|---|---|
| Relative imports | ../../utils/math | Nearby files | Simple, no extra config | Becomes messy in deep folders |
| TypeScript path aliases | @lib/utils/math | Large TypeScript projects | Readable, maintainable | Needs config in multiple tools |
| npm package imports | lodash | External dependencies or published internal packages | Standard module resolution | Requires package setup |
| Monorepo workspace packages | @company/shared | Large multi-package repos | Strong boundaries, reusable packages | More setup than simple aliases |
lib/... vs @lib/...
| Style | Example | Notes |
|---|---|---|
| Plain alias | lib/utils/date | Can be confused with a real package |
| Prefixed alias | @lib/utils/date | Usually clearer and more explicit |
paths vs Webpack alias
| Tool | What it affects |
|---|---|
tsconfig.json paths | TypeScript compiler and editor tooling |
Webpack resolve.alias | Bundle-time module resolution |
You often need both.
Cheat Sheet
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@lib/*": ["lib/src/*"],
"@browser/*": ["lib/src/browser/*"],
"@server/*": ["lib/src/server/*"],
"@universal/*": ["lib/src/universal/*"]
}
}
}
Quick rules
- Use
baseUrlwithpaths. @alias/*maps toreal/path/*.- The
*should usually appear on both sides. pathshelps TypeScript resolve imports.- Webpack usually needs matching
resolve.aliassettings. - Jest and other tools may also need matching alias config.
- Prefer aliases like
@lib/*over plain names likelib/*.
Example
import { Something } from "@lib/browser/foo";
resolves to:
lib/src/browser/foo
Remember
If TypeScript works but the app fails at build or runtime, check whether your bundler or runtime also knows the alias.
FAQ
Is tsconfig.json enough to use path aliases?
Usually not. It is enough for TypeScript type checking and editor support, but bundlers like Webpack often need matching alias configuration.
Do I need baseUrl when using paths?
In most common setups, yes. It defines the base location from which non-relative imports are resolved.
Why does TypeScript compile, but Webpack says module not found?
Because TypeScript and Webpack resolve modules separately. Your alias is likely configured in tsconfig.json but not in Webpack.
Should I use lib/* or @lib/*?
@lib/* is usually clearer because it looks like a deliberate alias instead of a package name.
Can I use path aliases in a monorepo?
Yes. They are very common in monorepos for shared code, especially when combined with a base tsconfig file.
Do path aliases change the emitted JavaScript import paths?
Not by themselves in a way that solves runtime resolution everywhere. They mainly affect how TypeScript understands module paths.
Can I create separate aliases for browser, server, and universal code?
Yes. This is a practical way to make environment boundaries clear, for example @browser/*, @server/*, and @universal/*.
Mini Project
Description
Create a small TypeScript project that replaces deep relative imports with path aliases. This demonstrates how to configure tsconfig.json and Webpack so both the compiler and bundler understand the same import paths.
Goal
Set up a working alias-based import system for shared TypeScript code in a multi-folder project.
Requirements
- Create a
lib/srcfolder with at least one utility module. - Configure
tsconfig.jsonwithbaseUrland a path alias. - Import the utility module using the alias instead of a relative path.
- Add matching alias resolution in Webpack.
- Build successfully without using
../../in the import.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.