TypeScript 57 🔷 Path Mapping and BaseUrl
A relative import like ../../../../utils/format is hard to read, fragile to move, and easy to get wrong. A path alias like @utils/format is stable, readable, and independent of the file’s location. The baseUrl and paths options in tsconfig.json are how TypeScript declares these aliases, and they are one of the most-used features in large TypeScript projects. But they come with a subtlety that catches every developer the first time: the aliases are a TypeScript-only mechanism. The compiler resolves them, but the runtime does not. The bundler, the test runner, the Node process, and the IDE all resolve imports with their own rules, and the aliases must be declared for each of them. This chapter covers baseUrl and paths in detail, the resolution rules, the wildcard patterns, the interaction with the bundler and with test runners, the alternatives (imports field, subpath exports), and the pitfalls that produce “Cannot find module” errors at build time or runtime.
Key point: baseUrl sets the base directory for non-relative imports. paths declares a map from an alias pattern to a list of target paths. The paths are resolved relative to baseUrl if it is set, or relative to the tsconfig.json if it is not. The alias is a compile-time mapping only — TypeScript rewrites nothing, and the emitted JavaScript keeps the alias. The bundler, the runtime, and the test runner must all be configured with the same alias, or the import fails after compilation. For new projects, the imports field in package.json is the modern alternative that TypeScript and the bundler both read, which avoids the duplication.
Why path aliases matter
A path alias replaces a relative path with a stable name. The name does not change when the file moves, and it does not depend on the file’s depth in the directory tree.
The relative path problem. A file at src/app/features/users/components/user-list/user-list.component.ts that imports a utility at src/utils/format.ts writes:
import { format } from "../../../../utils/format";
The four .. segments are the distance from the file to the src directory. If the file moves one level up, the import breaks. If the utility moves, the import breaks. The path is a computation of the directory distance, and the distance is fragile.
The alias solution. With an alias for src/utils, the import becomes:
import { format } from "@utils/format";
The import is a stable name. The file can move anywhere under src, and the import still works. The alias hides the directory structure and makes the import about the module, not about the location.
Why the aliases are a project convention. The @ prefix is a convention that distinguishes internal aliases from package imports. @utils/format is clearly not an npm package, and format from the format package is clearly external. The convention is not enforced by the resolver, but it makes the imports readable.
Why aliases improve refactoring. Moving a file that uses aliases does not require updating the imports in the file. Moving the target of an alias requires updating the alias in one place. The alias is a single point of change, and the relative path is a distributed one.
Why aliases are not always the right choice. For a small project with a shallow directory structure, relative paths are fine and the alias is overhead. For a large project with deep nesting, the alias is the difference between readable imports and a wall of ../. The threshold is a matter of judgment, and the common convention is to use aliases for the top-level directories and relative paths for the local ones.
Why the aliases are not resolved by Node. Node has no concept of the alias. The alias is declared in tsconfig.json, which Node does not read. When the emitted JavaScript contains import { format } from "@utils/format", Node tries to find a package called @utils/format in node_modules, which does not exist. The runtime error is the symptom of the missing configuration.
Why the alias duplication is the main pitfall. The
pathsoption is a TypeScript-specific mechanism. Every other tool — the bundler, the test runner, the linter, the runtime — resolves imports with its own rules and does not readtsconfig.json‘spaths. The aliases must be declared for each. Theimportsfield inpackage.jsonis the modern alternative that both TypeScript and the bundler read, which removes the duplication.
baseUrl and paths
The two options work together. baseUrl is the base directory, and paths is the map of aliases.
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@app/*": ["app/*"],
"@utils/*": ["utils/*"],
"@models/*": ["models/*"]
}
}
}
With this configuration, an import of @utils/format resolves to src/utils/format, and an import of @app/users/user-list resolves to src/app/users/user-list.
Why baseUrl is the base. The paths targets are relative to baseUrl. "utils/*" under baseUrl: "./src" means ./src/utils/*. Without a baseUrl, the targets are relative to the tsconfig.json file.
Why paths needs the wildcard. The * in @utils/* and utils/* captures the part of the import path that varies. An import of @utils/format matches @utils/* with * = format, and the target utils/* becomes utils/format. The wildcard is the mechanism for a prefix mapping.
Why the paths are an array. Each alias maps to a list of targets. The resolver tries them in order and uses the first that exists. The list is the fallback mechanism: "@utils/*": ["utils/*", "shared/utils/*"] tries utils/* first, then shared/utils/*.
Why the resolution is relative to baseUrl. The targets in paths are not absolute paths. They are resolved against baseUrl, which makes the alias configuration portable — the project can be moved without changing the paths, as long as the baseUrl is relative.
Why baseUrl alone is sometimes used. A baseUrl without paths makes non-relative imports resolve from the baseUrl. An import of utils/format resolves to src/utils/format without an @ prefix. This is the older convention and is less common today because it makes internal imports indistinguishable from package imports.
Why paths works without baseUrl in modern TypeScript. Since TypeScript 4.1, the paths option works without a baseUrl. The targets are resolved relative to the tsconfig.json. The baseUrl is still useful for the non-relative imports and for the legacy convention, but it is not required for the aliases.
Why the order of the paths matters. The resolver tries the paths in the order they appear in the paths object. A more specific alias should come before a more general one.
{
"paths": {
"@utils/special/*": ["utils/special/*"],
"@utils/*": ["utils/*"]
}
}
An import of @utils/special/format matches the first alias, which is the more specific one. If the general alias came first, the specific one would never match. The order is the specificity order.
The resolution algorithm
The paths resolution is a pattern match, and the algorithm is worth understanding because it explains the surprises.
The match. An import path is compared against each alias pattern in the order they appear in paths. The pattern with a * is a prefix match: the part before the * must match the beginning of the import, and the part after must match the end. The * captures the middle.
The substitution. The captured part is substituted into the target pattern, producing the candidate path. The candidate is resolved against baseUrl, and the resolver checks whether the file exists with the usual extension candidates.
The fallback. If the candidate does not exist, the next target in the array is tried. If none of the targets exist, the next alias pattern is tried. If no alias matches, the resolver falls back to the usual resolution — the package in node_modules or the relative path.
Why the fallback to node_modules matters. An alias that does not match does not break the import. The resolver continues with the standard resolution. This is why a package import and an alias can coexist without conflict: the alias patterns are distinct, and each import matches at most one.
Why the extension candidates matter. The paths target does not include an extension. The resolver appends the usual candidates — .ts, .tsx, .d.ts — and uses the first that exists. This is why the alias is written without an extension and the file is found.
Why the pattern must be exact. A typo in the alias pattern — "@utlis/*" — means the alias never matches, and the import of @utils/format falls back to node_modules, where it is not found. The error is “Cannot find module,” and the cause is the typo. The alias and the import must use the exact same prefix.
Why the alias is case-sensitive. The alias patterns and the import paths are case-sensitive, matching the file system on Linux. A file Utils/format.ts and an alias @utils/* do not match on a case-sensitive file system. The case must be consistent.
Why the pattern can be an exact path. A paths entry without a * is an exact match. "@config": ["config.ts"] matches the import @config exactly, not @config/anything. The exact form is useful for a single file that should be imported by a short name.
Configuring the bundler
The bundler resolves imports at build time, and it does not read tsconfig.json‘s paths. The aliases must be declared in the bundler’s configuration.
Vite. The resolve.alias option maps the aliases.
// vite.config.ts
import { defineConfig } from 'vite';
import path from 'node:path';
export default defineConfig({
resolve: {
alias: {
'@app': path.resolve(__dirname, 'src/app'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@models': path.resolve(__dirname, 'src/models'),
},
},
});
Each alias maps to an absolute path resolved at build time. The path.resolve is relative to the configuration file’s directory, which is the project root.
Webpack. The resolve.alias option is the equivalent.
// webpack.config.js
const path = require('path');
module.exports = {
resolve: {
alias: {
'@app': path.resolve(__dirname, 'src/app'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
},
};
The configuration is the same idea, expressed in Webpack’s format.
esbuild. The alias option is used with the build API.
// build.js
require('esbuild').build({
entryPoints: ['src/index.ts'],
bundle: true,
alias: {
'@app': './src/app',
'@utils': './src/utils',
},
});
The aliases are relative to the working directory.
Why the duplication is necessary. The bundler does not read tsconfig.json. The aliases are a build-time configuration, and each tool has its own. The duplication is the cost of the paths mechanism.
Why the alias must be consistent. The TypeScript alias and the bundler alias must resolve to the same file. If they differ — the alias maps to one directory in TypeScript and another in the bundler — the build fails or the runtime errors. The two configurations are a pair, and they must agree.
Why the alias should be checked after a change. A change to the alias in one configuration but not the other is a common mistake. The error is usually “Cannot find module” at build time, which points to the mismatch. The fix is to check both configurations.
Configuring the test runner
The test runner resolves imports with its own rules, and the aliases must be declared for it as well.
Jest. The moduleNameMapper option maps the aliases.
// jest.config.js
module.exports = {
moduleNameMapper: {
'^@app/(.*)$': '<rootDir>/src/app/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
},
};
The pattern uses a regular expression and a replacement. The $1 is the captured part.
Vitest. The resolve.alias option is the same as Vite’s, because Vitest is built on Vite.
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'node:path';
export default defineConfig({
resolve: {
alias: {
'@app': path.resolve(__dirname, 'src/app'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
},
});
The configuration is shared with Vite, which is why a Vitest project usually has one configuration for both.
Why the test runner needs the aliases. A test file that imports @utils/format uses the alias, and the test runner resolves it. Without the mapping, the test fails with “Cannot find module,” even though the application builds.
Why the test aliases are often forgotten. The application builds and runs, and the tests fail with a resolution error. The alias configuration in the bundler is not used by the test runner, and the test runner’s configuration is a separate file. The mistake is common, and the fix is a one-line addition.
Why the jest.config.js regex must be anchored. The ^ and $ in the pattern anchor the match to the start and end of the import path. Without the anchors, the pattern matches anywhere in the path, which produces unexpected matches. The anchored form is the correct one.
The imports field alternative
The imports field in package.json is the modern alternative to paths. It is read by TypeScript and by the bundler, which removes the duplication.
// package.json
{
"imports": {
"#utils/*": "./src/utils/*.ts",
"#app/*": "./src/app/*.ts",
"#config": "./src/config.ts"
}
}
An import of #utils/format resolves to ./src/utils/format.ts in both TypeScript and the bundler. The configuration is declared once.
Why the # prefix. The # prefix is reserved for the imports field. A #-prefixed import always refers to the imports mapping, which makes it unambiguous and distinguishes it from package imports.
Why the imports field is read by both. The field is part of the Node.js specification, and the bundlers and TypeScript honor it. The alias is declared once, and every tool resolves it the same way.
Why the .ts extension in the target. The imports field’s targets are files, and the extension is included. The .ts extension is the source file, which TypeScript resolves and the bundler processes. The .js extension would be the emitted file, which is correct only if the target is the built output.
Why the imports field is preferred for new projects. It removes the duplication between tsconfig.json and the bundler configuration, which is the most common source of alias errors. The cost is that the aliases use #, which is less conventional than @ but is the standard for the field.
Why the imports field is not universal. Some tools and some configurations do not read it, and some projects are on older versions that predate it. The paths option remains the more widely supported mechanism, and the choice depends on the project’s constraints.
Why the two can coexist. A project can use paths for the aliases and imports for the internal aliases, or migrate gradually. The two mechanisms are independent, and the resolver handles both.
Why the imports field must be in the package’s root. The field is read from the package.json of the package that contains the import. For a project, the root package.json is the one that matters. For a workspace, each package has its own.
Why the duplication is the fundamental problem. TypeScript, the bundler, the test runner, the linter, and the runtime each resolve imports with their own rules. The
pathsoption is TypeScript’s mechanism, and it is not read by the others. Theimportsfield is the shared mechanism, and it is the modern answer. Until every tool reads theimportsfield, the duplication is a fact, and the discipline is to keep the configurations in sync.
Complete Example Session
// ============================================
// PART 1: THE TSCONFIG
// ============================================
// tsconfig.json
// {
// "compilerOptions": {
// "baseUrl": "./src",
// "paths": {
// "@app/*": ["app/*"],
// "@utils/*": ["utils/*"],
// "@models/*": ["models/*"],
// "@config": ["config.ts"]
// }
// }
// }
// ============================================
// PART 2: THE IMPORTS
// ============================================
import { format } from "@utils/format";
import type { User } from "@models/user";
import { config } from "@config";
import { UserList } from "@app/features/users/user-list";
// ============================================
// PART 3: THE RESOLUTION
// ============================================
// @utils/format
// matches @utils/* with * = format
// target utils/* becomes utils/format
// resolved against baseUrl ./src
// candidate ./src/utils/format.ts
// exists → resolved
// @config
// exact match
// target config.ts
// resolved against baseUrl ./src
// candidate ./src/config.ts
// ============================================
// PART 4: THE VITE CONFIG
// ============================================
// vite.config.ts
// import { defineConfig } from 'vite';
// import path from 'node:path';
//
// export default defineConfig({
// resolve: {
// alias: {
// '@app': path.resolve(__dirname, 'src/app'),
// '@utils': path.resolve(__dirname, 'src/utils'),
// '@models': path.resolve(__dirname, 'src/models'),
// '@config': path.resolve(__dirname, 'src/config.ts'),
// },
// },
// });
// ============================================
// PART 5: THE JEST CONFIG
// ============================================
// jest.config.js
// module.exports = {
// moduleNameMapper: {
// '^@app/(.*)$': '<rootDir>/src/app/$1',
// '^@utils/(.*)$': '<rootDir>/src/utils/$1',
// '^@models/(.*)$': '<rootDir>/src/models/$1',
// '^@config$': '<rootDir>/src/config.ts',
// },
// };
// ============================================
// PART 6: THE imports FIELD ALTERNATIVE
// ============================================
// package.json
// {
// "imports": {
// "#app/*": "./src/app/*.ts",
// "#utils/*": "./src/utils/*.ts",
// "#models/*": "./src/models/*.ts",
// "#config": "./src/config.ts"
// }
// }
import { format } from "#utils/format";
import type { User } from "#models/user";
import { config } from "#config";
// Read by TypeScript and the bundler.
// No duplication.
// ============================================
// PART 7: THE ORDER OF THE PATHS
// ============================================
// {
// "paths": {
// "@utils/special/*": ["utils/special/*"], ← more specific
// "@utils/*": ["utils/*"] ← more general
// }
// }
//
// An import of @utils/special/format matches the first.
// If the general came first, the specific would never match.
// ============================================
// PART 8: THE FALLBACK ARRAY
// ============================================
// {
// "paths": {
// "@utils/*": ["utils/*", "shared/utils/*"]
// }
// }
//
// The resolver tries utils/* first.
// If the file does not exist, it tries shared/utils/*.
// ============================================
// PART 9: THE MIGRATION FROM paths TO imports
// ============================================
// Step 1: Add the imports field to package.json
// Step 2: Change the imports from @ to #
// Step 3: Remove the paths from tsconfig
// Step 4: Remove the alias from the bundler
// Step 5: Verify the build and the tests
// ============================================
// PART 10: WHAT NOT TO DO
// ============================================
// Don't declare the alias in tsconfig only
// The bundler fails.
// Don't forget the test runner
// The tests fail with "Cannot find module."
// Don't use a case-mismatched alias
// The resolver is case-sensitive.
// Don't put a general alias before a specific one
// The specific one never matches.
// Don't include an extension in the paths target
// The resolver appends the extension.
// Don't confuse the @ convention with a scope
// @utils/format is an alias, not an npm scope.
The ten parts cover the tsconfig, the imports, the resolution, the bundler and test configurations, the imports field, the order, the fallback, the migration, and the anti-patterns.
Quick Reference
The Options
| Option | Purpose |
|---|---|
baseUrl | Base for non-relative imports |
paths | Alias map |
paths without baseUrl | Relative to tsconfig.json |
imports (in package.json) | Shared alias mechanism |
resolve.alias (bundler) | Bundler aliases |
moduleNameMapper (Jest) | Test runner aliases |
The paths Syntax
| Form | Example | Matches |
|---|---|---|
| Prefix | "@utils/*": ["utils/*"] | @utils/format |
| Exact | "@config": ["config.ts"] | @config |
| Multiple targets | "@utils/*": ["utils/*", "shared/utils/*"] | Fallback |
The imports Syntax
| Form | Example | Matches |
|---|---|---|
| Prefix | "#utils/*": "./src/utils/*.ts" | #utils/format |
| Exact | "#config": "./src/config.ts" | #config |
Tool Configurations
| Tool | Option |
|---|---|
| TypeScript | paths |
| Vite | resolve.alias |
| Webpack | resolve.alias |
| esbuild | alias |
| Jest | moduleNameMapper |
| Vitest | resolve.alias |
The Resolution Algorithm
| Step | Action |
|---|---|
| 1 | Match the import against each pattern in order |
| 2 | Substitute the captured part into the target |
| 3 | Resolve the target against baseUrl |
| 4 | Try the extension candidates |
| 5 | Fall back to the next target, then the next pattern |
| 6 | Fall back to standard resolution |
Best Practices
✅ Do This:
// Use baseUrl and paths together
{ "baseUrl": "./src", "paths": { "@utils/*": ["utils/*"] } } // ✅
// Use the @ convention for internal aliases
import { format } from "@utils/format"; // ✅
// Duplicate the alias in the bundler
// vite.config.ts: resolve.alias // ✅
// Duplicate the alias in the test runner
// jest.config.js: moduleNameMapper // ✅
// Or use the imports field
{ "imports": { "#utils/*": "./src/utils/*.ts" } } // ✅
// Put specific aliases before general ones
{ "paths": { "@utils/special/*": [...], "@utils/*": [...] } } // ✅
❌ Don’t Do This:
// Don't declare the alias in tsconfig only
{ "paths": { "@utils/*": ["utils/*"] } } // bundler fails // ⚠️
// Don't use a case-mismatched alias
import { format } from "@Utils/format"; // case-sensitive // ⚠️
// Don't include an extension in the target
{ "paths": { "@utils/*": ["utils/*.ts"] } } // wrong // ⚠️
// Don't put a general alias first
{ "paths": { "@utils/*": [...], "@utils/special/*": [...] } } // ⚠️
// Don't confuse an alias with an npm scope
import { format } from "@utils/format";
// @utils is not a package; it is a path alias // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Alias in tsconfig only | Bundler fails | Duplicate in bundler |
| Test runner not configured | Tests fail | Add moduleNameMapper |
| Case mismatch | Alias never matches | Match the case |
| Extension in target | Wrong resolution | Remove the extension |
| General before specific | Specific never matches | Reorder |
baseUrl wrong | Wrong resolution | Check the base |
| Alias typo | Falls back to node_modules | Match exactly |
imports field in the wrong package | Not read | Root package.json |
Real-World Examples
1. Basic alias
{ "paths": { "@utils/*": ["utils/*"] } }
2. With baseUrl
{ "baseUrl": "./src", "paths": { "@utils/*": ["utils/*"] } }
3. Exact alias
{ "paths": { "@config": ["config.ts"] } }
4. Multiple targets
{ "paths": { "@utils/*": ["utils/*", "shared/utils/*"] } }
5. Vite alias
resolve: { alias: { '@utils': path.resolve(__dirname, 'src/utils') } }
6. Jest mapper
moduleNameMapper: { '^@utils/(.*)$': '<rootDir>/src/utils/$1' }
7. The imports field
{ "imports": { "#utils/*": "./src/utils/*.ts" } }
8. Specific before general
{ "paths": { "@utils/special/*": ["utils/special/*"], "@utils/*": ["utils/*"] } }
9. The # import
import { format } from "#utils/format";
10. The @ import
import { format } from "@utils/format";
Visual: The paths Resolution
┌──────────────────────────────────────────────────────────┐
│ import { format } from "@utils/format" │
│ │ │
│ ▼ │
│ Match against paths: │
│ "@app/*" → no match │
│ "@utils/*" → match, * = "format" │
│ │
│ Substitute into the target: │
│ "utils/*" → "utils/format" │
│ │
│ Resolve against baseUrl (./src): │
│ "./src/utils/format" │
│ │
│ Try the extension candidates: │
│ ./src/utils/format.ts ✅ exists │
│ │
│ Resolved. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Duplication
┌──────────────────────────────────────────────────────────┐
│ tsconfig.json │
│ paths: { "@utils/*": ["utils/*"] } │
│ │ │
│ └── TypeScript resolves this │
│ │
│ vite.config.ts │
│ resolve.alias: { '@utils': 'src/utils' } │
│ │ │
│ └── Vite resolves this at build time │
│ │
│ jest.config.js │
│ moduleNameMapper: { '^@utils/(.*)$': ... } │
│ │ │
│ └── Jest resolves this for the tests │
│ │
│ Three configurations, one alias. │
│ All must agree. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The imports Field Alternative
┌──────────────────────────────────────────────────────────┐
│ package.json │
│ imports: { "#utils/*": "./src/utils/*.ts" } │
│ │ │
│ ├── TypeScript reads it │
│ ├── Vite reads it │
│ ├── Webpack reads it │
│ └── esbuild reads it │
│ │
│ import { format } from "#utils/format" │
│ │ │
│ └── Every tool resolves the same way │
│ │
│ One configuration, one alias. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: Order of the Paths
┌──────────────────────────────────────────────────────────┐
│ { │
│ "paths": { │
│ "@utils/special/*": ["utils/special/*"], ← first │
│ "@utils/*": ["utils/*"] ← second │
│ } │
│ } │
│ │
│ import "@utils/special/format" │
│ matches the first pattern → utils/special/format │
│ │
│ import "@utils/format" │
│ does not match the first (no "special/") │
│ matches the second → utils/format │
│ │
│ Specific first, general second. │
│ │
├──────────────────────────────────────────────────────────┤
│ REVERSED │
│ │
│ { │
│ "paths": { │
│ "@utils/*": ["utils/*"], ← first │
│ "@utils/special/*": ["utils/special/*"] ← second │
│ } │
│ } │
│ │
│ import "@utils/special/format" │
│ matches the first pattern → utils/special/format │
│ (the general pattern also matches "special/format") │
│ │
│ The specific pattern is never reached. │
│ │
└──────────────────────────────────────────────────────────┘
Visual: The Failure Modes
┌──────────────────────────────────────────────────────────┐
│ COMPILE-TIME FAILURE │
│ │
│ tsconfig.json has no paths for the alias. │
│ Error: Cannot find module '@utils/format'. │
│ │
├──────────────────────────────────────────────────────────┤
│ BUILD-TIME FAILURE │
│ │
│ tsconfig has the paths, the bundler does not. │
│ Error: Failed to resolve import '@utils/format'. │
│ │
├──────────────────────────────────────────────────────────┤
│ TEST FAILURE │
│ │
│ tsconfig and bundler have it, Jest does not. │
│ Error: Cannot find module '@utils/format'. │
│ │
├──────────────────────────────────────────────────────────┤
│ RUNTIME FAILURE │
│ │
│ Everything compiles, Node runs the output. │
│ Error: Cannot find package '@utils/format'. │
│ │
│ Each failure is a missing configuration. │
│ │
└──────────────────────────────────────────────────────────┘
Summary
| Option | Purpose |
|---|---|
baseUrl | Base for non-relative imports |
paths | Alias map |
imports field | Shared alias mechanism |
resolve.alias | Bundler aliases |
moduleNameMapper | Jest aliases |
| Tool | Reads paths | Reads imports |
|---|---|---|
| TypeScript | ✅ | ✅ |
| Vite | ❌ | ✅ |
| Webpack | ❌ | ✅ |
| esbuild | ❌ | ✅ |
| Jest | ❌ | ⚠️ |
Key takeaways:
baseUrlsets the base directory andpathsdeclares the aliases — the two work together, and the targets are resolved againstbaseUrl- The
pathstargets are resolved relative tobaseUrl— or relative to thetsconfig.jsonifbaseUrlis not set - The alias is a compile-time mapping only — TypeScript does not rewrite the import, and the emitted JavaScript keeps the alias
- The bundler, test runner, and runtime each resolve with their own rules — the aliases must be declared for each, and the configurations must agree
- The
importsfield is the modern alternative — it is read by TypeScript and the bundler, which removes the duplication - The order of the paths matters — a specific alias must come before a general one, or the specific one never matches
- The alias is case-sensitive — the pattern and the import must use the exact same case
- The target should not include an extension — the resolver appends the usual candidates (
.ts,.tsx,.d.ts) - A typo in the alias falls back to
node_modules— the error is “Cannot find module,” and the cause is the mismatch - The failure mode points to the missing configuration — compile-time, build-time, test-time, or runtime, and each is a different tool’s configuration
Remember: Path mapping replaces fragile relative paths with stable aliases, and the aliases are declared in tsconfig.json for the compiler. But the compiler is not the only tool that resolves imports. The bundler, the test runner, and the runtime each have their own resolution, and the aliases must be declared for each. The imports field is the modern alternative that both TypeScript and the bundler read. Use the imports field for new projects, and keep the configurations in sync for the ones that use paths.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!