Die tsconfig.json ist das Steuerzentrum jedes TypeScript-Projekts: Sie entscheidet, welche Sprachversion am Ende emittiert wird, wie Module aufgelöst werden, wie streng der Typchecker arbeitet und welche Dateien überhaupt zum Projekt gehören. Wer die zentralen Optionen versteht, spart sich endlose Stunden Debugging an scheinbar zufälligen Modul- oder Typfehlern. Dieser Artikel führt dich durch die wichtigsten Felder, zeigt eine pragmatische Minimal-Konfiguration und eine moderne strict-Variante für ernsthafte Projekte — und erklärt die Stolperfallen rund um moduleResolution, paths und das Zusammenspiel mit Bundlern wie Vite oder Next.js. Stand der Beispiele: TypeScript 5.8 (Mai 2026).

Was die tsconfig.json macht

Die tsconfig.json markiert die Wurzel eines TypeScript-Projekts. Sie legt fest, welche Dateien zum Projekt gehören und mit welchen Optionen der Compiler tsc sie verarbeitet. Ohne diese Datei kennt TypeScript weder dein Ziel-JS noch deine Modulauflösung — tsc fällt dann auf hartcodierte Defaults zurück, die selten zu deinem Setup passen.

Auffinden: Wenn du tsc ohne Argumente startest, sucht der Compiler im aktuellen Verzeichnis nach tsconfig.json und arbeitet sich anschliessend die Verzeichnis-Hierarchie nach oben durch, bis er fündig wird. Du kannst auch explizit eine Konfiguration angeben:

bash terminal
tsc --project ./tsconfig.build.json
# oder kurz:
tsc -p ./tsconfig.build.json

Wichtig: Sobald du tsc mit konkreten Dateinamen aufrufst (tsc src/index.ts), wird tsconfig.json ignoriert. Das ist ein häufiger Stolperstein in CI-Skripten — bau immer per tsc -p.

Generator tsc --init: Eine sinnvolle Start-Konfiguration bekommst du mit:

bash terminal
npx tsc --init

Die generierte Datei enthält viele auskommentierte Optionen mit Kurzbeschreibung — sehr nützlich zum Stöbern, aber für reale Projekte zu lang. In der Praxis startet man besser mit einer schlanken Datei oder einer fertigen Base-Konfiguration über extends (siehe Specials am Ende).

Minimalkonfiguration

Die kleinste sinnvolle tsconfig.json für ein modernes Node-Projekt sieht so aus:

jsonc tsconfig.json
{
    "compilerOptions": {
        "target": "es2022",
        "module": "nodenext",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "outDir": "dist"
    },
    "include": ["src/**/*"]
}

Was hier passiert:

  • target: es2022 — Output ist modernes JavaScript, Node 18+ versteht das ohne Probleme.
  • module: nodenext — Echtes ESM-/CommonJS-Handling nach Node-Spezifikation; setzt automatisch moduleResolution: nodenext mit.
  • strict: true — Alle strikten Checks aktivieren (siehe Abschnitt 5).
  • esModuleInterop: true — Interop zwischen ESM-Imports und CommonJS-Modulen; quasi Pflicht.
  • skipLibCheck: true.d.ts-Dateien aus node_modules werden nicht typgeprüft, was Build-Zeit massiv spart.
  • outDir: dist — Kompiliertes JavaScript landet unter dist/.

Mehr brauchst du für den Anfang nicht. Alles Weitere ist Optimierung.

target und lib

target bestimmt, welche JavaScript-Version tsc als Output erzeugt — wichtig, weil neueres Syntax-Sugar (optional chaining, nullish coalescing, top-level await, decorators) je nach Target entweder direkt emittiert oder zu älterem JS umgeschrieben wird.

lib bestimmt, welche eingebauten Typen (DOM-API, ES-API) dir während der Typprüfung zur Verfügung stehen. Wenn du lib nicht setzt, leitet TypeScript es aus target ab.

TargetVerfügbare Sprachfeatures (Auswahl)Typischer Einsatz
es5Klassisches ES5, kein let/const-Down-Level.Legacy-Browser, IE11.
es2015Klassen, let/const, Promises, Module.Sehr alte Bundler-Targets.
es2017async/await nativ.Node 8+, alte Edge.
es2020Optional chaining, nullish coalescing, BigInt.Node 14+, alle modernen Browser.
es2022Class fields, at(), top-level await (in Modulen).Node 18+ — guter Default heute.
esnextAlles, was TypeScript aktuell kennt.Nur sinnvoll mit Bundler, der das wieder runterkompiliert.

Praxisregel: Wenn ein Bundler (Vite, esbuild, Rollup, Next.js) am Werk ist, ist target: esnext oder es2022 fast immer die richtige Wahl — der Bundler kümmert sich um Browser-Kompatibilität. Wenn tsc selbst der finale Compiler ist (klassische Node-Library), wähle das niedrigste target, das deine Ziel-Node-Version garantiert versteht.

lib getrennt setzen: In einer Browser-App brauchst du explizit dom:

jsonc tsconfig.json
{
    "compilerOptions": {
        "target": "es2022",
        "lib": ["es2022", "dom", "dom.iterable"]
    }
}

In einer reinen Node-Library lässt du dom weg — dann meckert TypeScript zu Recht, wenn jemand versehentlich document.querySelector schreibt.

module und moduleResolution

Diese beiden Optionen sind die häufigste Fehlerquelle in tsconfigs. Sie bestimmen:

  • module — wie der Output aussieht (CommonJS require oder ESM import).
  • moduleResolution — wie TypeScript Imports auflöst (welche Dateien finde ich hinter import x from 'y').
WertOutput-FormEmpfohlen für
commonjsrequire/module.exportsKlassische Node-Projekte ohne ESM.
esnextReines import/exportProjekte mit Bundler.
node16 / nodenextMix aus CJS und ESM, abhängig von package.json typeModerne Node-Libraries mit Dual-Publishing.
preserve (seit 5.4)Lässt Import-Syntax unverändertWenn ein Bundler den Rest macht.

moduleResolution-Werte:

  • node — alter Default, kennt keine .mjs/.cjs-Unterscheidung. Nicht mehr verwenden.
  • node16 / nodenext — Node-konformes Verhalten, inkl. expliziter Datei-Extensions in Imports.
  • bundler — seit TypeScript 5.0, der pragmatische Modus für Vite, Webpack, Next.js, esbuild. Erlaubt extensionslose Imports und Auflösung über package.json-exports, ohne dass tsc selbst Code erzeugt.
  • classic — Legacy, ignorieren.

Faustregel:

  • Bundler im Spiel (Vite, Next, Remix, Webpack)? → module: esnext + moduleResolution: bundler.
  • Reine Node-Library, die per tsc gebaut wird? → module: nodenext (moduleResolution: nodenext wird automatisch mitgesetzt).
  • Etwas dazwischen? → meistens auch nodenext.

Die strict-Family

strict: true ist der wichtigste Schalter in der ganzen Datei. Er aktiviert auf einen Schlag alle strikten Typchecks:

FlagWirkung
noImplicitAnyVariablen ohne erschliessbaren Typ werden zum Fehler.
strictNullChecksnull und undefined sind eigene Typen, keine impliziten Mitglieder anderer Typen.
strictFunctionTypesStrenge bivariante vs. kontravariante Parameter-Prüfung.
strictBindCallApply.bind/.call/.apply werden typgeprüft.
strictPropertyInitializationKlassen-Properties müssen im Konstruktor zugewiesen oder als optional markiert werden.
noImplicitThisthis mit implizitem any wird zum Fehler.
alwaysStrictEmittiert "use strict" in jeder Datei.
useUnknownInCatchVariablescatch (e) typisiert e als unknown statt any.

Empfehlung ohne Wenn und Aber: In jedem neuen Projekt strict: true setzen. Die geringe Mehrarbeit beim Schreiben wird durch wegfallende Laufzeitfehler um Grössenordnungen aufgewogen — gerade strictNullChecks ist der Hauptgrund, warum sich TypeScript-Code von JavaScript-Code spürbar sicherer anfühlt.

Wer noch strenger fahren will, ergänzt:

jsonc tsconfig.json
{
    "compilerOptions": {
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "noFallthroughCasesInSwitch": true,
        "noImplicitOverride": true,
        "noImplicitReturns": true,
        "exactOptionalPropertyTypes": true
    }
}

noUncheckedIndexedAccess ist besonders wertvoll: Es zwingt dich, bei Array- und Objekt-Index-Zugriffen undefined zu behandeln.

rootDir, outDir, include, exclude

Diese vier Optionen bestimmen den Datei-Scope des Projekts.

  • rootDir — der Quellordner. Wenn nicht gesetzt, leitet TypeScript ihn aus den include-Patterns ab. Wichtig, weil outDir die Verzeichnis-Struktur relativ zu rootDir spiegelt.
  • outDir — wohin der kompilierte JavaScript-Output geschrieben wird.
  • include — Glob-Patterns, welche Dateien zum Projekt gehören.
  • exclude — Glob-Patterns, was ausgeschlossen wird. Default schliesst node_modules, bower_components, jspm_packages und outDir aus.
jsonc tsconfig.json
{
    "compilerOptions": {
        "rootDir": "src",
        "outDir": "dist"
    },
    "include": ["src/**/*"],
    "exclude": ["**/*.test.ts", "**/*.spec.ts"]
}

Stolperfalle rootDir: Wenn du Imports ausserhalb von rootDir machst (z. B. import { x } from '../shared/utils', während rootDir auf src zeigt), bricht tsc mit einer kryptischen Meldung ab. Lösung: entweder rootDir weiter oben ansetzen oder die Quelldatei nach src ziehen.

include vs. files: Statt Glob-Patterns kannst du auch files: ["src/index.ts", "src/server.ts"] schreiben — sinnvoll, wenn du wirklich nur einzelne Einstiegspunkte hast (selten).

paths und baseUrl

Aliase wie @/components/Button statt ../../../components/Button machen Imports lesbar. Konfiguriert wird das über paths plus optional baseUrl.

jsonc tsconfig.json
{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/*": ["src/*"],
            "@components/*": ["src/components/*"],
            "@utils": ["src/utils/index.ts"]
        }
    }
}

Seit TypeScript 5.0 ist baseUrl optional, wenn du paths mit relativen Patterns angibst — der Compiler löst dann ab dem Verzeichnis der tsconfig.json auf.

Die grosse Falle: paths ist nur eine Information für den Typchecker. Zur Laufzeit weiss weder Node noch der Browser etwas davon. Wenn dein Bundler (Vite, Webpack, esbuild) den Alias nicht parallel auflöst, knallt es beim Start mit „module not found".

Lösung — Alias muss zweimal eingetragen werden:

ts vite.config.ts
import { defineConfig } from "vite";
import path from "node:path";

export default defineConfig({
    resolve: {
        alias: {
            "@": path.resolve(__dirname, "src"),
        },
    },
});

Tools wie vite-tsconfig-paths oder tsconfig-paths für Node lesen die Konfiguration direkt aus tsconfig.json aus — komfortabler, aber eine zusätzliche Abhängigkeit.

tsBuildInfoFile, incremental, noEmit

Drei Optionen, die du je nach Workflow brauchst.

  • incremental: true — TypeScript speichert nach jedem Build einen kleinen Snapshot (.tsbuildinfo), aus dem der nächste Build nur noch die Diffs berechnet. Beschleunigt grosse Projekte enorm.
  • tsBuildInfoFile — wohin diese Info geschrieben wird. Standard ist neben dem Output. In Monorepos oft sinnvoll, sie an einen zentralen Ort zu legen.
  • noEmit: true — TypeScript prüft Typen, schreibt aber keine Dateien. Das ist der Normalfall, wenn ein Bundler den eigentlichen Build macht: tsc --noEmit läuft dann als reiner Linter in der CI.
jsonc tsconfig.json
{
    "compilerOptions": {
        "incremental": true,
        "tsBuildInfoFile": "./.tsbuildinfo",
        "noEmit": true
    }
}

In package.json taucht das dann typischerweise so auf:

jsonc package.json
{
    "scripts": {
        "build": "vite build",
        "typecheck": "tsc --noEmit"
    }
}

Beispiel: moderne strict-tsconfig für Node-Library

Eine produktionsreife Konfiguration für eine klassische, per tsc gebaute Node-Library:

jsonc tsconfig.json
{
    "compilerOptions": {
        "target": "es2022",
        "module": "nodenext",
        "lib": ["es2022"],
        "rootDir": "src",
        "outDir": "dist",
        "declaration": true,
        "declarationMap": true,
        "sourceMap": true,
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "noImplicitOverride": true,
        "noFallthroughCasesInSwitch": true,
        "exactOptionalPropertyTypes": true,
        "verbatimModuleSyntax": true,
        "isolatedModules": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "incremental": true
    },
    "include": ["src/**/*"],
    "exclude": ["**/*.test.ts", "dist"]
}

Highlights:

  • declaration/declarationMap.d.ts-Dateien plus Mappings, damit Konsumenten der Library direkt in deine Quellen springen können.
  • verbatimModuleSyntax — erzwingt explizites import type/export type, klare Trennung von Werten und Typen.
  • isolatedModules — stellt sicher, dass jede Datei auch einzeln kompiliert werden kann (Pflicht für Babel/SWC-Konsumenten).
  • forceConsistentCasingInFileNames — verhindert die klassische macOS-vs-Linux-Falle, bei der User.ts und user.ts auf dem Mac dasselbe sind, in der CI aber nicht.

Beispiel: tsconfig für Vite/Next.js/Bundler

Wenn ein Bundler den eigentlichen Build macht und tsc nur Typchecker ist:

jsonc tsconfig.json
{
    "compilerOptions": {
        "target": "es2022",
        "module": "esnext",
        "moduleResolution": "bundler",
        "lib": ["es2022", "dom", "dom.iterable"],
        "jsx": "react-jsx",
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "noEmit": true,
        "allowImportingTsExtensions": true,
        "verbatimModuleSyntax": true,
        "isolatedModules": true,
        "esModuleInterop": true,
        "resolveJsonModule": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "incremental": true,
        "baseUrl": ".",
        "paths": {
            "@/*": ["src/*"]
        }
    },
    "include": ["src", "vite.config.ts"]
}

Highlights:

  • moduleResolution: bundler + module: esnext — der Bundler löst auf, tsc schreibt nichts.
  • noEmit: true — TypeScript ist hier nur Typchecker.
  • allowImportingTsExtensions — erlaubt import x from './foo.ts', was Vite und moderne Bundler problemlos verarbeiten.
  • jsx: react-jsx — moderner JSX-Transform ohne import React from 'react'-Pflicht.

Besonderheiten

extends für Konfig-Vererbung nutzen.

Mit extends erbst du eine Basis-Konfiguration aus einem npm-Paket oder einer lokalen Datei. Die offiziellen Bases liegen unter @tsconfig/* — etwa @tsconfig/strictest, @tsconfig/node20 oder @tsconfig/vite-react. Damit reduziert sich deine projekt-spezifische Datei auf wenige Zeilen.

references für Monorepo-Projekt-Strukturen.

Mit references verlinkst du mehrere TypeScript-Projekte miteinander, sodass tsc --build sie in der richtigen Reihenfolge inkrementell kompiliert. Pflicht-Begleiter ist composite: true in jedem referenzierten Sub-Projekt — ohne das verweigert tsc die Project-References.

verbatimModuleSyntax löst zwei Vorgänger-Flags ab.

Seit TypeScript 5.0 ersetzt verbatimModuleSyntax die alten Flags importsNotUsedAsValues und preserveValueImports. Die Regel ist simpel: Jeder Import, der nur als Typ verwendet wird, muss mit import type deklariert sein. Klare Trennung, deterministischer Output.

isolatedModules erzwingt Bundler-Kompatibilität.

Tools wie Babel, SWC oder esbuild kompilieren jede Datei einzeln und sehen den restlichen Projektkontext nicht. isolatedModules: true stellt sicher, dass du keine Syntax verwendest, die für solche Single-File-Transpiler nicht eindeutig auflösbar ist — z. B. const-enums ohne Inlining.

skipLibCheck als pragmatischer Default.

skipLibCheck: true überspringt die Typprüfung aller .d.ts-Dateien aus node_modules. In der Praxis ist das fast immer richtig — fremde Type-Definitions sind nicht dein Problem, und der Build wird spürbar schneller. Der einzige Grund dagegen: du schreibst selbst Type-Definitions und willst sie mitgeprüft sehen.

allowImportingTsExtensions für expliziten .ts-Import.

Seit TypeScript 5.0 erlaubt allowImportingTsExtensions: true Imports wie import x from './foo.ts'. Voraussetzung: entweder noEmit: true oder emitDeclarationOnly: true. Praktisch in Vite/Deno/Bun-Umgebungen, die ohnehin .ts-Endungen erwarten.

module: nodenext setzt moduleResolution automatisch.

Sobald du module: nodenext (oder node16) wählst, wird moduleResolution automatisch auf denselben Wert gesetzt — beide gehören untrennbar zusammen. Manuelles Mischen wie module: nodenext + moduleResolution: bundler führt zu Fehlern.

Project-References mit composite: true für Monorepo-Builds.

In einem Monorepo mit mehreren Paketen aktivierst du in jedem Sub-Paket composite: true. Damit erzeugt tsc die nötigen Build-Infos und Declaration-Files, sodass das übergeordnete Projekt diese Pakete per references einbinden und gemeinsam mit tsc --build inkrementell kompilieren kann.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Grundlagen

Zur Übersicht