Skip to Content

Path aliases

If your tsconfig.json uses compilerOptions.paths (e.g. @/* β†’ ./src/*), the emit will contain those alias paths verbatim, and they won’t resolve at runtime. @ttsc/paths rewrites them into relative imports during compile.

The problem

// src/main.ts (source) import { value } from "@lib/value";

After a plain ttsc build:

// dist/main.js (broken) import { value } from "@lib/value"; // ← Node can't resolve this

The fix

npm install -D @ttsc/paths
// tsconfig.json { "compilerOptions": { "rootDir": "src", "outDir": "dist", "paths": { "@/*": ["./src/*"], "@lib/*": ["./src/modules/*"] }, "plugins": [{ "transform": "@ttsc/paths" }] } }

Now the emit is:

// dist/main.js import { value } from "./modules/value.js";

What it does and doesn’t do

  • Reads the same compilerOptions.paths, rootDir, and outDir that ttsc already uses.
  • Rewrites JavaScript-family emit (.js, .mjs, .cjs, .jsx) and .d.ts declarations.
  • Resolves extensionless path targets against .ts, .tsx, .mts, .cts, then .js, .jsx, .mjs, and .cjs source files.
  • No separate plugin config.
  • Only rewrites specifiers that match compilerOptions.paths; relative, absolute, and non-matching package specifiers are left alone.
  • Uses TypeScript-Go’s emitted runtime suffix for matched targets (.js, .mjs, .cjs, or .jsx depending on the source file and jsx mode).
  • Does not change source files: only the emit.

Common alternatives (and why this is simpler)

  • tsc-alias: post-build script. Works, but it’s a second pass. @ttsc/paths runs inside the compile.
  • Path remapping at runtime via tsconfig-paths/register: adds runtime cost and a startup step. @ttsc/paths produces normal relative imports.
  • Custom bundler config: if a bundler owns your build, that’s fine. @ttsc/paths is for projects that ship as plain Node packages.

Inspired by

typescript-transform-paths. Same goal, different compiler.

See also

Last updated on