Die wichtigsten Optionen im Detail
tsconfig.json ist die zentrale Steuerzentrale jedes Projekts mit TypeScript und entscheidet über Kompatibilität, Fehlerstrenge und Buildgeschwindigkeit. Dieser Artikel erklärt die wichtigsten Optionen wie target, module, strict, esModuleInterop und skipLibCheck im Detail, liefert eine sinnvolle Basiskonfiguration für neue Projekte und zeigt, wie sich Konfiguration mit extends sauber über ein Monorepo teilen lässt.
Inhaltsverzeichnis
- 1. Warum tsconfig.json die wichtigste Konfigurationsdatei ist
- 2. target und lib: welches JavaScript entsteht
- 3. module und moduleResolution: die Grundlagen
- 4. Die strict Familie: welche Flags wirklich zählen
- 5. esModuleInterop und skipLibCheck: zwei unterschätzte Schalter
- 6. outDir, rootDir und include/exclude: die Projektstruktur
- 7. Eine sinnvolle Basiskonfiguration für ein neues Projekt
- 8. extends: Konfiguration im Monorepo teilen
- 9. tsconfig.json Optionen im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum tsconfig.json die wichtigste Konfigurationsdatei ist
Wer tsc --init ausführt, bekommt eine tsconfig.json mit über hundert auskommentierten Optionen und keiner Orientierung, welche davon tatsächlich Priorität haben. Dabei entscheidet diese eine Datei über drei fundamentale Dinge: welches JavaScript am Ende aus dem Compiler herauskommt, wie streng TypeScript Fehler im eigenen Code aufspürt, und wie der Compiler mit Modulen, Bibliotheken und Drittanbieter-Typen umgeht. Ein falsch konfiguriertes target erzeugt Code, der in älteren Umgebungen crasht oder unnötig aufgebläht ist. Ein falsch konfiguriertes module erzeugt Importe, die im Zielsystem nicht auflösbar sind.
Für Magento- und Hyvä-Entwickler, die TypeScript für Build-Skripte, Admin-Tools oder Headless-Frontends einsetzen, ist tsconfig.json oft die einzige Stelle, an der projektweite Entscheidungen getroffen werden, ohne dass jede einzelne Datei angepasst werden muss. Die folgenden Abschnitte gehen die wichtigsten Optionen einzeln durch, zeigen eine sinnvolle Basiskonfiguration für neue Projekte und erklären, wie sich Konfiguration über mehrere Pakete in einem Monorepo mit extends teilen lässt.
2. target und lib: welches JavaScript entsteht
target legt fest, auf welche ECMAScript-Version der TypeScript-Compiler den Code herunterkompiliert, und ist damit die Option mit dem größten Einfluss auf die erzeugte Ausgabe. Bei ES5 werden Arrow Functions, Klassen, async/await und optional chaining in älteren, deutlich längeren Code übersetzt, was Bundle-Größe und Ausführungszeit spürbar erhöht. Bei einem modernen target wie ES2022 bleibt die Syntax weitgehend erhalten, weil moderne Node.js-Versionen und alle relevanten Browser diese Features nativ unterstützen. Die Faustregel: target so hoch wie möglich wählen, ausgerichtet an den tatsächlichen Zielumgebungen des Projekts, nicht an einem imaginären Worst Case.
lib ist von target unabhängig und bestimmt, welche globalen Typdeklarationen der Compiler kennt, etwa Promise, fetch oder DOM-Interfaces wie HTMLElement. Ein Node.js-Projekt ohne Browser-Code braucht kein DOM in lib, ein Frontend-Projekt dagegen schon. Fehlt eine benötigte lib, meldet der Compiler unbekannte Typen, obwohl der Code zur Laufzeit einwandfrei läuft, weil die Laufzeitumgebung die API längst bereitstellt.
# Compile the same source file with different target settings
$ tsc --target ES5 --module CommonJS greet.ts
$ cat greet.js
"use strict";
var greet = function (name) {
return "Hello, " + name;
};
# ES2022 target keeps modern syntax, smaller and closer to source
$ tsc --target ES2022 --module ESNext greet.ts
$ cat greet.js
const greet = (name) => `Hello, ${name}`;
# lib controls which global runtime APIs the compiler assumes exist
$ tsc --target ES2022 --lib ES2022,DOM greet.ts
3. module und moduleResolution: die Grundlagen
module bestimmt, in welchem Modulsystem der Compiler die Ausgabe erzeugt, etwa CommonJS mit require/module.exports oder ESNext mit nativen import/export-Anweisungen. Für Node.js-Projekte, die sowohl ESM- als auch CommonJS-Pakete konsumieren, ist NodeNext die richtige Wahl, weil der Compiler dann anhand der Dateiendung und des type-Felds in package.json entscheidet, welches Format erwartet wird.
moduleResolution steuert, nach welchem Algorithmus der Compiler Importe wie import { foo } from "./bar" zu tatsächlichen Dateien auflöst. Die Optionen Node10, NodeNext und Bundler unterscheiden sich vor allem darin, ob Dateiendungen bei relativen Importen erforderlich sind und wie package.json-exports-Felder interpretiert werden. Dieser Artikel geht auf die feineren Unterschiede der Modulauflösung nur kurz ein, ein eigener Beitrag zu Modulauflösung in TypeScript behandelt das Thema mit allen Randfällen im Detail. Für die tägliche Praxis reicht die Faustregel: NodeNext für Node.js-Bibliotheken und CLI-Tools, Bundler für Anwendungen, die von Vite, esbuild oder Webpack gebündelt werden.
4. Die strict Familie: welche Flags wirklich zählen
strict ist kein einzelnes Flag, sondern ein Sammelschalter, der eine ganze Gruppe strengerer Prüfungen gleichzeitig aktiviert, darunter strictNullChecks, noImplicitAny, strictFunctionTypes und strictPropertyInitialization. Ohne strictNullChecks behandelt TypeScript null und undefined als Unterwerte jedes Typs, wodurch die häufigste Laufzeitfehlerklasse in JavaScript, der Zugriff auf eine Eigenschaft von undefined, zur Kompilierzeit unsichtbar bleibt. noImplicitAny verhindert, dass Parameter ohne Typangabe stillschweigend zu any werden und damit jede Typprüfung für diesen Wert deaktivieren.
In bestehenden Projekten empfiehlt sich, strict schrittweise einzuführen: zuerst noImplicitAny, dann strictNullChecks, weil diese beiden Flags die meisten Fehler in ungetyptem Legacy-Code aufdecken. Neue Projekte sollten strict: true von Anfang an setzen, weil das Nachrüsten in einer wachsenden Codebasis exponentiell aufwendiger wird als das Einhalten von Anfang an.
// strict: false - these all compile without any error
function getUser(id) {
const user = users.find(u => u.id === id);
return user.name; // user might be undefined, no warning at all
}
let value;
value = 42;
value = "now a string"; // implicit any, TypeScript does not complain
// strict: true - TypeScript forces you to handle the edge cases
function getUser(id: number): string {
const user = users.find(u => u.id === id);
if (!user) {
throw new Error(`User ${id} not found`);
}
return user.name; // narrowed to defined, this access is safe
}
let typedValue: number;
typedValue = 42;
// typedValue = "now a string";
// Error: Type 'string' is not assignable to type 'number'.
5. esModuleInterop und skipLibCheck: zwei unterschätzte Schalter
esModuleInterop löst ein historisches Kompatibilitätsproblem zwischen CommonJS und ES-Modulen: Ohne diese Option erzwingt TypeScript für den Default-Import eines CommonJS-Pakets eine Syntax wie import * as express from "express", obwohl die meisten Bibliotheken einen echten Default-Export erwarten würden. Mit esModuleInterop: true funktioniert die natürlichere Schreibweise import express from "express" zuverlässig, weil der Compiler beim Kompilieren automatisch einen kompatiblen Wrapper erzeugt. Diese Option sollte in praktisch jedem neuen Projekt aktiviert sein, es sei denn, es handelt sich um eine reine ESM-Bibliothek ohne CommonJS-Abhängigkeiten.
skipLibCheck überspringt die Typprüfung innerhalb aller .d.ts-Dateien, also der Typdeklarationen aus node_modules. Ohne diese Option prüft der Compiler auch Typkonflikte zwischen unterschiedlichen Versionen derselben Bibliothek in verschachtelten Abhängigkeiten, was in großen Projekten spürbar Zeit kostet und Fehler meldet, auf die man als Anwendungsentwickler ohnehin keinen Einfluss hat. skipLibCheck: true ist in fast allen Projekten die richtige Wahl und beschleunigt den Build spürbar, ohne die Typsicherheit des eigenen Codes zu verringern.
6. outDir, rootDir und include/exclude: die Projektstruktur
rootDir legt fest, welches Verzeichnis als gemeinsame Wurzel aller Eingabedateien gilt, und bestimmt damit, wie die Ordnerstruktur im Ausgabeverzeichnis nachgebildet wird. outDir legt fest, wohin der kompilierte JavaScript-Code geschrieben wird. Ohne explizites rootDir berechnet der Compiler die gemeinsame Wurzel automatisch aus allen eingeschlossenen Dateien, was bei Testdateien außerhalb von src/ zu einer unerwartet tiefen Ordnerstruktur im Output führen kann.
include und exclude steuern, welche Dateien überhaupt Teil des Kompiliervorgangs sind. include nimmt Glob-Muster wie src/**/* entgegen, exclude schließt Muster wie node_modules, dist oder Testdateien explizit aus. Wichtig: exclude verhindert nicht, dass eine Datei importiert wird, sondern nur, dass sie eigenständig kompiliert wird. Eine ausgeschlossene Datei kann trotzdem über einen Import in den Kompiliervorgang hineingezogen werden, wenn eine eingeschlossene Datei sie referenziert.
project/
├── src/
│ ├── index.ts
│ ├── utils/
│ │ └── format.ts
│ └── components/
│ └── header.ts
├── dist/ # generated by outDir, mirrors the src structure
│ ├── index.js
│ ├── utils/
│ │ └── format.js
│ └── components/
│ └── header.js
└── tsconfig.json
# tsconfig.json excerpt
# "rootDir": "./src" -> only src/ is treated as the input root
# "outDir": "./dist" -> compiled output mirrors src/ under dist/
# "include": ["src/**/*"]
# "exclude": ["node_modules", "dist", "**/*.test.ts"]
$ tsc
# emits dist/index.js, dist/utils/format.js, dist/components/header.js
7. Eine sinnvolle Basiskonfiguration für ein neues Projekt
Für die meisten neuen TypeScript-Projekte, ob Node.js-Backend, Build-Skript oder Headless-Frontend-Integration, ist die folgende Basiskonfiguration ein solider Startpunkt. Sie kombiniert moderne Zielsemantik mit maximaler Fehlerstrenge, ohne unnötige Kompatibilitätslasten aus der Vergangenheit mitzuschleppen.
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"isolatedModules": true,
"noUncheckedIndexedAccess": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
Diese Konfiguration setzt strict und mehrere zusätzliche Prüfungen wie noUncheckedIndexedAccess, das Array- und Objektzugriffe über einen Index standardmäßig als möglicherweise undefined behandelt. declaration und declarationMap erzeugen .d.ts-Dateien mit Sourcemap-Unterstützung, was für jedes Paket wichtig ist, das von anderen TypeScript-Projekten importiert wird. Wer diese Basiskonfiguration als Ausgangspunkt nimmt und projektspezifisch anpasst, spart sich das mühsame Nachrüsten strenger Prüfungen in einer bereits gewachsenen Codebasis.
8. extends: Konfiguration im Monorepo teilen
In einem Monorepo mit mehreren Paketen, etwa einem API-Server, einer geteilten Utility-Bibliothek und einem Frontend, wiederholt sich ohne extends dieselbe Basiskonfiguration in jedem einzelnen Paket. Das extends-Feld löst dieses Problem, indem eine paketspezifische tsconfig.json eine gemeinsame Basisdatei referenziert und nur die Werte überschreibt, die tatsächlich abweichen, etwa rootDir, outDir oder lib. Alle anderen Optionen werden unverändert aus der Basisdatei übernommen.
Für Monorepos mit Build-Abhängigkeiten zwischen Paketen lohnt sich zusätzlich composite: true in Kombination mit references, weil TypeScript dann inkrementelle Builds über Paketgrenzen hinweg unterstützt: Ändert sich nur ein Paket, werden nicht alle abhängigen Pakete komplett neu kompiliert. Wichtig beim Einsatz von extends: relative Pfade wie include und exclude in der überschreibenden Datei werden relativ zur überschreibenden Datei aufgelöst, nicht relativ zur Basisdatei, ein häufiger Stolperstein bei tief verschachtelten Paketstrukturen.
// tsconfig.base.json at the monorepo root, shared by every package
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"composite": true
}
}
// packages/api/tsconfig.json extends the shared base file
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022"]
},
"include": ["src/**/*"],
"references": [
{ "path": "../shared" }
]
}
9. tsconfig.json Optionen im direkten Vergleich
Nicht jede Standardeinstellung von TypeScript ist für ein modernes Projekt die richtige Wahl. Die folgende Übersicht zeigt fünf Optionen, bei denen die riskante oder veraltete Einstellung nach wie vor häufig in bestehenden Projekten zu finden ist, und die empfohlene Alternative für neue oder modernisierte Konfigurationen.
| Option | Riskant / veraltet | Empfohlen | Effekt |
|---|---|---|---|
| target | ES5 | ES2022 | Kleinerer, moderner Output ohne unnötige Transpilierung |
| module | CommonJS | NodeNext / ESNext | Native ESM-Interop, zukunftssicher für Node.js |
| strict | false | true | null/undefined-Fehler zur Kompilierzeit statt zur Laufzeit |
| skipLibCheck | false | true | Spürbar schnellere Builds, keine Prüfung fremder .d.ts |
| moduleResolution | node (Node10) | bundler | Korrekte exports-Feld-Auflösung für Vite/esbuild-Projekte |
Der gemeinsame Nenner aller fünf Zeilen: Die riskantere Option war meist vor Jahren die einzig praktikable Wahl, etwa als Node.js selbst noch kein natives ESM unterstützte. Wer heute ein neues Projekt aufsetzt, sollte konsequent zu den empfohlenen Werten greifen und nur bei nachweisbarer Notwendigkeit davon abweichen, etwa bei Unterstützung sehr alter Browser oder Legacy-Build-Pipelines.
Mironsoft
TypeScript-Tooling, Build-Konfiguration und Headless-Integrationen für Magento
tsconfig.json richtig aufgesetzt, statt nur kopiert?
Wir richten TypeScript-Projekte, Build-Skripte und Monorepos für euer Magento- und Hyvä-Umfeld sauber ein, von der passenden Basiskonfiguration bis zu geteilten extends-Setups über mehrere Pakete hinweg.
tsconfig-Audit
Bestehende Konfiguration prüfen, riskante Defaults identifizieren
Monorepo-Setup
Geteilte Basiskonfiguration mit extends und project references
Build-Tooling
TypeScript in Vite, esbuild und CI-Pipelines sauber integrieren
10. Zusammenfassung
tsconfig.json ist keine Sammlung beliebiger Compiler-Schalter, sondern die zentrale Stelle, an der ein TypeScript-Projekt seine Zielumgebung, seine Fehlerstrenge und seine Modulstrategie definiert. target und lib bestimmen, welches JavaScript entsteht und welche Laufzeit-APIs bekannt sind. module und moduleResolution entscheiden, wie Importe aufgelöst werden. Die strict-Familie deckt die häufigsten Laufzeitfehler bereits zur Kompilierzeit auf, esModuleInterop und skipLibCheck lösen zwei praktische Kompatibilitäts- und Performance-Probleme, und outDir/rootDir strukturieren die Ausgabe sauber.
Für neue Projekte lohnt sich, die in diesem Artikel gezeigte Basiskonfiguration als Startpunkt zu übernehmen und mit strict von Anfang an zu arbeiten, statt sie später nachzurüsten. In Monorepos reduziert extends Duplikation drastisch und sorgt dafür, dass alle Pakete dieselben grundlegenden Compiler-Regeln teilen, während projektspezifische Details wie rootDir und outDir lokal überschrieben werden können.
tsconfig.json erklärt - Das Wichtigste auf einen Blick
target & lib
Modernes target wie ES2022 wählen, lib passend zur tatsächlichen Laufzeitumgebung setzen.
strict Familie
strict: true von Anfang an in neuen Projekten, schrittweise Migration in Altprojekten.
esModuleInterop & skipLibCheck
Fast immer aktivieren, für saubere Default-Imports und deutlich schnellere Builds.
extends im Monorepo
Gemeinsame Basisdatei mit paketspezifischen Overrides für rootDir und outDir.