paths
Maps import specifiers such as @/* to folders for type checking only.
"paths": { [pattern: string]: string[] }- Added in TypeScript 2.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$pattern | string | The specifier to intercept, for example "jquery" or "@/*". At most one asterisk may appear, and it captures the rest of the specifier. |
$locations | string[] | An ordered list of locations to try, resolved against baseUrl when set and against the tsconfig.json file otherwise (TypeScript 4.1+). An asterisk in a location is substituted with the text the pattern captured; entries are tried in order until one resolves. |
Return value
Adds extra lookup locations the checker tries before normal resolution. Emitted import specifiers are unchanged.
Errors
A pattern or substitution containing more than one asterisk is error TS5061. A mapping that resolves to nothing falls through to normal resolution and typically ends as TS2307, "Cannot find module".
Changelog
- PHP 4.1baseUrl is no longer required; without it, entries resolve relative to the tsconfig.json file's directory.
- PHP 6.0With baseUrl deprecated, any prefix that used to come from it should be written into the paths entries themselves.
paths re-maps import specifiers to lookup locations, so import { Button } from "@/components/Button" resolves to ./src/components/Button.tsx instead of a package in node_modules. Since TypeScript 4.1 the locations are relative to the tsconfig.json itself unless baseUrl says otherwise.
The thing that bites everyone
paths does not change emitted code. tsc writes from "@/components/Button" into the output exactly as you typed it. Nothing rewrites it. The option exists purely to tell TypeScript that some other tool already performs this mapping at runtime or bundle time.
So every alias must be declared twice — once here, and once in whatever actually resolves modules:
- Vite / Rollup:
resolve.alias - webpack:
resolve.alias - Jest:
moduleNameMapper - Plain Node: an
importsfield inpackage.json, or a loader
When the editor is happy and the test run says Cannot find module '@/…', this duplication is the reason.
Wildcards
A pattern may contain at most one *, which captures the remainder of the specifier and is substituted into each location. Locations are an ordered list, so ["./src/*", "./generated/*"] tries the second only if the first misses — useful for overlaying generated code onto handwritten code.
Example
// tsconfig.json
// { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
//
// vite.config.ts — the SAME mapping must exist here, or at runtime:
// resolve: { alias: { "@": path.resolve(__dirname, "src") } }
import { formatCents } from "@/money"; // -> ./src/money.ts
console.log(formatCents(1999));
// -> $19.99
// tsc emits `from "@/money"` UNCHANGED. Vite rewrites it; node does not.When to use it
- Replacing ../../../ chains with a stable @/ prefix across an app.
- Pointing an internal monorepo package at its source instead of its built output.
- Overlaying generated types onto handwritten ones by listing two locations in order.
- Redirecting a package to a vendored copy without touching every import site.
Frequently asked questions
Why does my tsconfig paths alias fail at runtime?
Because paths only informs TypeScript's type checker — tsc emits the aliased specifier unchanged. The bundler, test runner or Node loader must be given the same mapping, via resolve.alias, moduleNameMapper, or a package.json imports field.
Do I still need baseUrl to use paths?
No, since TypeScript 4.1 paths entries resolve relative to the tsconfig.json file when baseUrl is absent. baseUrl was deprecated in TypeScript 6.0, so fold any prefix it provided into the paths entries themselves.
Can a paths pattern have more than one wildcard?
No, at most one asterisk is allowed in a pattern and in each substitution, and more is error TS5061. The single asterisk captures the rest of the specifier and is substituted into each location in turn.
More examples
Declaring the same alias in every tool
Three files, one mapping, repeated by hand. Drift between them is the usual cause of an import that type checks in the editor but fails in the test run, so keep them next to each other in review.
// tsconfig.json
// "paths": { "@/*": ["./src/*"], "@test/*": ["./test/helpers/*"] }
// vite.config.ts
// resolve: { alias: { "@": "/src", "@test": "/test/helpers" } }
// jest.config.js
// moduleNameMapper: {
// "^@/(.*)$": "<rootDir>/src/$1",
// "^@test/(.*)$": "<rootDir>/test/helpers/$1"
// }
import { formatCents } from "@/money";
import { fixture } from "@test/fixtures";
console.log(formatCents(fixture.totalCents));
// -> $42.00Ordered fallbacks and a single-package redirect
A pattern without an asterisk matches one exact specifier, which is how you redirect a package name to a vendored copy. With an asterisk, the list is tried in order, letting generated files shadow handwritten ones without changing any import.
// tsconfig.json
// { "compilerOptions": { "paths": {
// "jquery": ["./vendor/jquery/dist/jquery"], // exact specifier, no wildcard
// "@app/*": ["./src/generated/*", "./src/app/*"]
// } } }
//
// "@app/schema" is looked up as ./src/generated/schema first;
// only if that misses does it fall back to ./src/app/schema.
import type { User } from "@app/schema"; // from ./src/generated
import { render } from "@app/render"; // falls back to ./src/app
const user: User = { id: 7, name: "Ada" };
console.log(render(user));
// -> Ada (#7)
Discussion