tsconfig.json erklaert: Die wichtigsten Optionen im Detail
<T>
type
TypeScript · tsconfig.json · Compiler Options · Build Tooling
tsconfig.json erklärt
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.

12 Min. Lesezeit target · module · strict · extends TypeScript 5.x · Node.js · Monorepo

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.

11. FAQ: tsconfig.json erklärt

1Was ist tsconfig.json und brauche ich sie überhaupt?
Die zentrale Konfigurationsdatei eines TypeScript-Projekts. Legt fest, welche Dateien kompiliert werden, welches JavaScript entsteht und wie streng Fehler gemeldet werden. Praktisch unverzichtbar ab mehr als einer Datei.
2Was steuert target konkret?
Die ECMAScript-Zielversion des Compilers. Niedrige targets wie ES5 erzeugen längeren, älteren Code. Moderne targets wie ES2022 behalten die Syntax weitgehend bei.
3Was ist der Unterschied zwischen module und moduleResolution?
module bestimmt das Ausgabeformat, moduleResolution den Algorithmus zur Auflösung von Importen zu Dateien. Zusammenhängend, aber unabhängige Optionen.
4Was aktiviert strict: true tatsächlich?
Ein Sammelschalter für strictNullChecks, noImplicitAny, strictFunctionTypes und weitere Prüfungen. Fängt die häufigsten Laufzeitfehler bereits zur Kompilierzeit ab.
5Warum ist esModuleInterop wichtig?
Erlaubt natürliche Default-Imports für CommonJS-Pakete über einen automatisch erzeugten Wrapper. Ohne diese Option sind umständlichere Namespace-Imports nötig.
6Was macht skipLibCheck und ist es sicher?
Überspringt die Typprüfung von .d.ts-Dateien in node_modules. Beschleunigt Builds deutlich und ist in fast allen Projekten unbedenklich.
7Was ist der Unterschied zwischen include/exclude und files?
include/exclude arbeiten mit Glob-Mustern für ganze Verzeichnisse. files listet einzelne Dateien explizit auf, meist nur bei sehr kleinen Projekten sinnvoll.
8Wie arbeiten outDir und rootDir zusammen?
rootDir definiert die Wurzel aller Eingabedateien, outDir das Zielverzeichnis. Der Compiler bildet die Struktur unterhalb rootDir eins zu eins in outDir nach.
9Wie funktioniert extends in einem Monorepo?
Eine paketspezifische tsconfig.json referenziert eine gemeinsame Basisdatei und überschreibt nur abweichende Werte. Alle übrigen Optionen werden unverändert übernommen.
10Kann ein Projekt mehrere tsconfig.json Dateien haben?
Ja, größere Projekte nutzen oft mehrere Dateien für unterschiedliche Zwecke, etwa Build und Tests, meist über extends miteinander verbunden.