Path Mapping und Aliases für saubere Imports
<T>
type
TypeScript · Path Mapping · Import Aliases · tsconfig.json
Path Mapping und Aliases für saubere Imports
von der ../../../-Hölle zu @/components

Die paths-Option in tsconfig.json macht Importe wie @/components lesbar und stabil, löst aber nur die Typprüfung, nicht die Modulauflösung zur Laufzeit. Dieser Artikel zeigt, wie Aliase korrekt bis in Vite, Webpack, Node und Testrunner durchgezogen werden, damit nach dem Build kein Modul-nicht-gefunden-Fehler mehr auftaucht.

14 Min. Lesezeit tsconfig paths · baseUrl · Bundler-Alias TypeScript 5.x · Vite · Webpack · Node.js

1. Das Problem mit tiefen relativen Imports (../../../-Ketten)

In gewachsenen TypeScript-Projekten importieren tief verschachtelte Module fast zwangsläufig über lange relative Pfade wie ../../../../components/ui/Button. Jede Verschiebung einer Datei in eine andere Ordnerebene bricht diese Pfade, weil die Anzahl der ../-Segmente exakt zur Ordnerstruktur passen muss. Code-Reviews werden dadurch unnötig schwer lesbar, denn ein Importpfad wie ../../../utils/format verrät nichts über den fachlichen Zusammenhang zwischen den Modulen, sondern nur über ihre zufällige physische Position im Dateisystem.

Das eigentliche Problem ist struktureller Natur: relative Importpfade koppeln den Code an seinen Speicherort statt an seine Bedeutung. Refactorings, bei denen ganze Verzeichnisse verschoben werden, erzwingen oft hunderte Änderungen an Importpfaden, obwohl sich an der eigentlichen Logik nichts geändert hat. Zusätzlich produzieren IDEs und Merge-Tools bei parallelen Refactorings häufig Konflikte, weil dieselben Ketten in vielen Dateien gleichzeitig angepasst werden müssen. Ein Alias wie @/components/ui/Button löst dieses Problem, weil er unabhängig vom Speicherort der importierenden Datei immer gleich aussieht.


// Before: fragile relative import chain that breaks on every file move
import { Button } from '../../../../components/ui/Button';
import { formatCurrency } from '../../../utils/format';
import { useCart } from '../../hooks/useCart';

// After: clean alias import, stable regardless of file location
import { Button } from '@/components/ui/Button';
import { formatCurrency } from '@/utils/format';
import { useCart } from '@/hooks/useCart';

2. Die "paths"-Option in tsconfig.json erklärt

Die paths-Option im Compiler-Abschnitt der tsconfig.json ordnet Importmuster wie @/* einem oder mehreren physischen Pfaden relativ zu baseUrl zu. TypeScript verwendet diese Zuordnung ausschließlich für zwei Zwecke: die Typauflösung während der Entwicklung und die Fehlerprüfung beim Kompilieren. Der Compiler durchsucht die Kandidatenpfade in der angegebenen Reihenfolge und verwendet den ersten Treffer, der eine passende Deklarationsdatei oder Quelldatei liefert. Das Sternchen fungiert als Platzhalter, der exakt einem Pfadsegment-Rest entspricht und in jedem Eintrag der Zielliste an derselben Stelle eingesetzt wird.

Wichtig ist, dass paths rein additiv zur normalen Modulauflösung wirkt und keine neuen Module erzeugt, sondern nur bestehende Dateien unter einem zusätzlichen, kürzeren Namen erreichbar macht. Mehrere Zielpfade pro Muster sind erlaubt, etwa um während einer Migration sowohl einen alten als auch einen neuen Ordner zu unterstützen. Diese Flexibilität wird jedoch schnell zur Falle, wenn Entwickler annehmen, dass allein das Setzen dieser Option ausreicht, damit der Alias auch im gebauten Code funktioniert.


{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@/components/*": ["src/components/*"],
      "@/utils/*": ["src/utils/*"],
      "@config": ["src/config/index.ts"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.tsx"]
}

3. baseUrl und das Zusammenspiel mit paths

baseUrl definiert das Wurzelverzeichnis, von dem aus alle nicht-relativen Importe und alle in paths definierten Zielpfade aufgelöst werden. Ohne gesetztes baseUrl werden paths-Einträge von TypeScript je nach Version inkonsistent behandelt. Ein üblicher Wert ist "baseUrl": "." in Kombination mit "paths": { "@/*": ["src/*"] }, wodurch alle Importe unter src/ über den kurzen Alias erreichbar werden, ohne dass der Ordnername src in jedem Importpfad wiederholt werden muss.

Seit TypeScript 4.1 ist baseUrl bei der Verwendung von paths technisch nicht mehr zwingend erforderlich, dennoch empfiehlt es sich, den Wert explizit zu setzen, weil viele Bundler und Editor-Plugins ihn weiterhin als Referenzpunkt erwarten. Ein häufiger Fehler entsteht, wenn baseUrl auf src gesetzt wird, aber paths-Einträge zusätzlich noch einmal mit src/ beginnen. Das führt zu doppelt aufgelösten Pfaden wie src/src/components, die TypeScript zwar für Editor-Zwecke still korrigiert, aber in manchen Bundler-Konfigurationen zu echten Auflösungsfehlern führen.

4. Warum TypeScript allein Imports zur Laufzeit nicht umschreibt

Der zentrale Denkfehler beim Einsatz von Path Mapping lautet: TypeScript ist ein Typchecker und Transpiler, kein Modul-Bundler. Der Befehl tsc prüft Importpfade gegen die paths-Konfiguration, um Typen korrekt aufzulösen, schreibt aber beim Kompilieren zu JavaScript den Importpfad selbst unverändert in die Ausgabedatei. Ein Import wie import { Button } from '@/components/ui/Button' bleibt nach tsc im JavaScript exakt from '@/components/ui/Button', denn tsc besitzt keine Logik, um @/* durch den tatsächlichen relativen Pfad zu ersetzen.

Zur Laufzeit versucht Node.js oder der Browser, dieses Modul über den regulären Modulauflösungsalgorithmus zu finden, kennt aber weder @ als Präfix noch die Zuordnung aus der tsconfig.json. Das Ergebnis ist ein Cannot find module '@/components/ui/Button'-Fehler, obwohl tsc beim Kompilieren keinen einzigen Fehler gemeldet hat. Dieser Bruch zwischen erfolgreicher Kompilierung und scheiterndem Laufzeitverhalten überrascht viele Entwickler, die zum ersten Mal Aliase einsetzen, weil TypeScript selbst keinerlei Warnung ausgibt, dass eine zusätzliche Werkzeugkette für die Laufzeitauflösung notwendig ist.

5. Passende Bundler-Konfiguration: Vite resolve.alias

Vite löst Module über Rollup und einen eigenen Entwicklungsserver auf, der komplett unabhängig von der TypeScript-Konfiguration arbeitet, sofern kein Plugin die Brücke schlägt. Die direkteste Lösung ist, dieselben Aliase manuell im Abschnitt resolve.alias der vite.config.ts zu deklarieren, mit dem path-Modul und fileURLToPath, um einen absoluten Pfad zum src-Verzeichnis zu erzeugen. Diese Konfiguration muss inhaltlich exakt zur paths-Zuordnung in der tsconfig.json passen, sonst funktioniert die Typprüfung im Editor, während der Vite-Build mit einem Auflösungsfehler abbricht.

Alternativ automatisiert das Plugin vite-tsconfig-paths diesen Abgleich, indem es die tsconfig.json zur Build-Zeit einliest und die Aliase automatisch aus den paths-Einträgen ableitet. Das reduziert Duplikation und verhindert, dass beide Konfigurationen bei künftigen Änderungen auseinanderlaufen. Für einfache Projekte mit wenigen, stabilen Aliasen ist die manuelle Variante oft ausreichend und transparenter, während sich das Plugin bei komplexen Monorepos mit vielen Paketen und häufig wechselnden Pfaden auszahlt.


// vite.config.ts
import { defineConfig } from 'vite';
import { fileURLToPath, URL } from 'node:url';

export default defineConfig({
  resolve: {
    alias: {
      // Must match the "@/*" -> "src/*" mapping in tsconfig.json exactly
      '@': fileURLToPath(new URL('./src', import.meta.url)),
      '@config': fileURLToPath(new URL('./src/config/index.ts', import.meta.url)),
    },
  },
});

6. Passende Bundler-Konfiguration: Webpack resolve.alias / tsconfig-paths

Webpack kennt die tsconfig.json ebenfalls nicht von sich aus. Der native Weg ist, im Abschnitt resolve.alias der webpack.config.js dieselben Zuordnungen wie in paths manuell zu hinterlegen, jeweils als absoluter Pfad über path.resolve. Da Loader wie ts-loader oder babel-loader lediglich TypeScript in JavaScript übersetzen, ohne die Importpfade zu kennen, findet die eigentliche Alias-Auflösung ausschließlich in Webpacks Modulauflösungsphase statt, komplett getrennt von der TypeScript-Kompilierung selbst.

Das Paket tsconfig-paths-webpack-plugin übernimmt diesen Abgleich automatisch, indem es zur Build-Zeit die paths-Einträge aus der tsconfig.json liest und in Webpacks Resolver einspeist. Es wird im Abschnitt resolve.plugins registriert und benötigt keine manuell doppelt gepflegten Pfade. Bei Projekten mit mehreren tsconfig.json-Dateien, etwa in Monorepos mit tsconfig.base.json und projektspezifischen Erweiterungen, muss der Parameter configFile explizit gesetzt werden, da das Plugin sonst die falsche Konfigurationsdatei liest und die Aliase ins Leere laufen.


// webpack.config.ts
import path from 'node:path';
import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin';

export default {
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
    alias: {
      // Manual fallback, kept in sync with tsconfig.json "paths"
      '@': path.resolve(__dirname, 'src'),
    },
    plugins: [
      new TsconfigPathsPlugin({ configFile: path.resolve(__dirname, 'tsconfig.json') }),
    ],
  },
};

7. Node.js-Runtime abgleichen: tsc-alias, tsconfig-paths/register oder Subpath Imports

Wird TypeScript-Code direkt über tsc zu JavaScript kompiliert und anschließend mit reinem node ausgeführt, greift keine der bisherigen Bundler-Lösungen, weil Node.js kein eigenes Alias-System besitzt. Das Werkzeug tsc-alias läuft nach tsc als zweiter Schritt und schreibt alle Alias-Importe in den kompilierten .js-Dateien in relative Pfade um, sodass Node.js sie ohne weitere Hilfe versteht. Der Aufruf erfolgt typischerweise als tsc && tsc-alias im Build-Skript, wodurch die Kompilierung und die Pfadumschreibung als getrennte, nachvollziehbare Schritte ablaufen.

Für die Entwicklung ohne vorherigen Kompilierschritt registriert tsconfig-paths/register zur Laufzeit einen Node.js-Loader-Hook, der paths-Einträge live aus der tsconfig.json liest und Alias-Importe im laufenden Prozess auflöst, etwa über node -r tsconfig-paths/register dist/index.js oder in Kombination mit ts-node. Eine dritte, bundlerunabhängige Alternative sind native Node.js Subpath Imports über den imports-Block in der package.json, die mit einem #-Präfix arbeiten und seit Node 12 ohne Zusatzpaket funktionieren, allerdings eine eigene, von tsconfig.json getrennte Pflege erfordern.


# Install tsc-alias alongside TypeScript
npm install --save-dev tsc-alias typescript

# package.json build script: compile, then rewrite alias imports to relative paths
# "build": "tsc && tsc-alias -p tsconfig.json"
npm run build

# Result in dist/index.js:
# before: require("@/utils/format")
# after:  require("../utils/format")
node dist/index.js

8. Typische Stolperfallen und Debugging von "Module not found"

Der häufigste Fehler ist die eingangs beschriebene Lücke zwischen Typprüfung und Laufzeit: Der Editor zeigt keine Fehler, tsc --noEmit läuft grün durch, aber node dist/index.js bricht mit Cannot find module ab, weil die Bundler- oder Node-Konfiguration nicht nachgezogen wurde. Ein zweiter klassischer Fall betrifft Testrunner wie Jest oder Vitest, die ihre eigene Modulauflösung mitbringen und paths aus der tsconfig.json grundsätzlich ignorieren, sofern nicht explizit moduleNameMapper (Jest) oder resolve.alias in der Vitest-Konfiguration gesetzt ist. Tests schlagen dann isoliert fehl, obwohl Anwendung und Build einwandfrei funktionieren.

Ein dritter Stolperstein entsteht bei ESLint-Importregeln wie import/no-unresolved, die ohne den eslint-import-resolver-typescript denselben Alias fälschlich als nicht auflösbar melden. Zum systematischen Debugging hilft es, den Fehler entlang der Werkzeugkette zu isolieren: zuerst tsc --noEmit für die reine Typprüfung, dann den Bundler-Build separat, dann den tatsächlichen Laufzeitstart, und schließlich die Testkonfiguration einzeln prüfen. Wer den fehlerhaften Schritt so eingrenzt, findet fast immer eine fehlende oder abweichende Alias-Deklaration in genau einem Werkzeug der Kette.

9. Path-Mapping-Ansätze im Vergleich

Die folgende Übersicht vergleicht typische Szenarien, in denen Path Mapping entweder korrekt konfiguriert ist oder an einer einzelnen fehlenden Stelle in der Werkzeugkette bricht. Der Unterschied zwischen der riskanten und der empfohlenen Spalte entscheidet meist darüber, ob ein Alias in allen Umgebungen, also Editor, Build, Laufzeit und Tests, gleichzeitig funktioniert oder nur in einer einzigen.

Szenario Riskant Empfohlen Vorteil
Importstil ../../../components/Button @/components/Button Stabil bei Datei-Verschiebungen
tsconfig paths allein paths ohne Bundler-Alias paths + passender Bundler-Alias Läuft auch nach dem Build
tsc-Build für Node tsc ohne tsc-alias tsc && tsc-alias Node findet kompilierte Module
Testrunner moduleNameMapper vergessen moduleNameMapper synchron halten Tests scheitern nicht isoliert
Pfad-Reichweite Absolute Pfade ab Projekt-Root Gescopter @/*-Alias Kein Leaking von Build-Details

In der Praxis reicht eine einzige vergessene Stelle, damit Path Mapping in einer Umgebung funktioniert und in einer anderen bricht. Wer die Tabelle als Checkliste bei jeder neuen Umgebung durchgeht, Editor, Bundler, Node-Laufzeit und Testrunner, vermeidet die typischen "funktioniert bei mir"-Situationen, die bei Aliasen besonders häufig auftreten.

Mironsoft

TypeScript-Tooling, Build-Konfiguration und Frontend-Infrastruktur

Aliase, die in jeder Umgebung wirklich funktionieren?

Wir analysieren eure tsconfig.json, Bundler- und Testkonfiguration und richten Path Mapping so ein, dass Editor, Build, Laufzeit und Tests konsistent denselben Aliasen folgen, ohne Modul-nicht-gefunden-Überraschungen nach dem Deploy.

tsconfig-Audit

paths, baseUrl und moduleResolution auf Konsistenz und Fehlerquellen prüfen

Bundler-Integration

Vite, Webpack und Node-Build so einrichten, dass Aliase überall auflösen

Test- & CI-Setup

Jest/Vitest moduleNameMapper und ESLint-Resolver synchron mit tsconfig halten

10. Zusammenfassung

Path Mapping und Aliases lösen ein reales Lesbarkeits- und Wartbarkeitsproblem: @/components/Button statt ../../../../components/Button macht Importpfade unabhängig von der physischen Dateiposition. Die paths-Option in der tsconfig.json ist dafür der richtige Startpunkt, löst aber ausschließlich die Typprüfung im Editor und beim Kompilieren - sie schreibt Importpfade beim Übersetzen zu JavaScript nicht um. Ohne eine passende Ergänzung im Bundler, in Node.js oder im Testrunner bleibt der Alias im gebauten Code als reiner Text stehen und führt zu Laufzeitfehlern, die TypeScript selbst nicht anzeigt.

Der entscheidende Hebel ist Konsistenz über die gesamte Werkzeugkette: dieselbe Alias-Definition muss in tsconfig.json, im Bundler (Vite resolve.alias oder Webpack mit tsconfig-paths-webpack-plugin), in der Node-Laufzeit (tsc-alias oder tsconfig-paths/register) und im Testrunner (Jest moduleNameMapper, Vitest resolve.alias) hinterlegt sein. Wer diese vier Stellen synchron hält oder ein Plugin nutzt, das die Konfiguration automatisch aus der tsconfig.json ableitet, vermeidet die klassischen "Cannot find module"-Fehler nach dem Deploy zuverlässig.

Path Mapping und Aliases - Das Wichtigste auf einen Blick

paths löst nur Typen

tsconfig.json paths steuert Typauflösung und Editor-Autocomplete, schreibt aber keine JavaScript-Importe um.

Bundler-Alias erforderlich

Vite resolve.alias oder Webpack mit tsconfig-paths-webpack-plugin müssen exakt zu paths passen.

Node-Laufzeit abgleichen

tsc-alias für kompilierten Code, tsconfig-paths/register für Entwicklung ohne Build-Schritt.

Tests nicht vergessen

Jest moduleNameMapper und Vitest resolve.alias separat pflegen oder aus tsconfig ableiten.

11. FAQ: Path Mapping und Aliases für saubere Imports

1Was macht die paths-Option in tsconfig.json genau?
Ordnet Importmuster wie @/* einem physischen Pfad relativ zu baseUrl zu, ausschließlich für die Typauflösung. Erzeugt keine neuen Module und schreibt keine JavaScript-Importe um.
2Warum funktioniert mein Alias im Editor, aber nicht mit node?
tsc prüft nur Typen und lässt den Importpfad beim Kompilieren unverändert. Node.js kennt die tsconfig-Zuordnung nicht und wirft Cannot find module.
3Brauche ich baseUrl, wenn ich paths verwende?
Seit TypeScript 4.1 technisch nicht mehr zwingend, aber empfohlen, weil viele Bundler und Editor-Plugins es weiterhin als Referenzpunkt erwarten.
4Wie bekomme ich Alias-Imports in Vite zum Laufen?
resolve.alias in vite.config.ts manuell setzen, identisch zu tsconfig paths, oder das Plugin vite-tsconfig-paths verwenden.
5Wie bekomme ich Alias-Imports in Webpack zum Laufen?
resolve.alias manuell mit path.resolve pflegen oder tsconfig-paths-webpack-plugin in resolve.plugins registrieren.
6Was macht tsc-alias genau?
Läuft nach tsc und schreibt Alias-Importe in den kompilierten .js-Dateien in relative Pfade um, damit Node.js sie versteht.
7Unterschied zwischen tsc-alias und tsconfig-paths/register?
tsc-alias schreibt Pfade einmalig nach dem Build um. tsconfig-paths/register löst Aliase live zur Laufzeit auf, ideal ohne separaten Build-Schritt.
8Warum meldet Jest Cannot find module @/... trotz funktionierendem Build?
Jest bringt eine eigene Modulauflösung mit und ignoriert tsconfig paths ohne explizit gesetztes moduleNameMapper.
9Sind native Node.js Subpath Imports eine Alternative?
Ja, über imports in der package.json mit #-Präfix, seit Node 12 ohne Zusatzpaket, aber getrennt von tsconfig.json zu pflegen.
10Wie debugge ich Module not found nach Alias-Einführung?
Entlang der Werkzeugkette isolieren: tsc --noEmit, dann Bundler-Build, dann Laufzeitstart, dann Testkonfiguration einzeln prüfen.