ESLint fuer TypeScript richtig konfigurieren
<T>
type
TypeScript · ESLint · Static Analysis · CI/CD
ESLint fuer TypeScript richtig konfigurieren
Flat Config, type-aware Regeln und eine CI ohne Doppelarbeit

Ein falsch konfiguriertes ESLint-Setup uebersieht entweder echte Typfehler oder bremst jeden Commit mit unnoetig langsamen type-aware Regeln aus. Mit der modernen Flat Config, dem Paket typescript-eslint und einer klaren Trennung zwischen schnellem Syntax-Lint und vollstaendigem Type-Check laesst sich beides gleichzeitig erreichen: verlaessliche Fehlererkennung und ein Entwicklungsworkflow, der nicht ausbremst.

14 Min. Lesezeit typescript-eslint · Flat Config · type-aware Linting ESLint 9 · TypeScript 5.x · CI/CD

1. Warum ESLint TypeScript-Syntax nicht von Haus aus versteht

ESLint wurde urspruenglich fuer JavaScript entwickelt und nutzt intern den Parser Espree, der ausschliesslich gueltige ECMAScript-Syntax versteht. Interfaces, Generics, Type-Annotationen, enum-Deklarationen und as-Type-Assertions existieren in der JavaScript-Grammatik schlicht nicht. Sobald Espree auf function identity<T>(value: T): T trifft, bricht das Parsing mit einem Syntaxfehler ab, lange bevor irgendeine Lint-Regel ueberhaupt zum Einsatz kommt. Ohne einen TypeScript-faehigen Parser ist ESLint fuer .ts- und .tsx-Dateien schlicht funktionsunfaehig.

Das Paket @typescript-eslint/parser loest dieses Problem, indem es den TypeScript-Compiler nutzt, um Quellcode in einen ESLint-kompatiblen AST zu uebersetzen, der zusaetzlich alle TypeScript-spezifischen Knoten enthaelt. @typescript-eslint/eslint-plugin liefert darauf aufbauend eigene Regeln, die genau diese Knoten auswerten: unbenutzte Type-Parameter erkennen, inkonsistente Interface- vs. Type-Alias-Nutzung melden oder auf redundante Type-Annotationen hinweisen. Beide Pakete sind seit einiger Zeit im gemeinsamen Meta-Paket typescript-eslint gebuendelt, das Parser, Plugin und Konfigurations-Helfer aus einer Hand bereitstellt und Versionskonflikte zwischen den Einzelpaketen vermeidet.

2. Flat Config mit typescript-eslint aufsetzen

Seit ESLint 9 ist die Flat Config in eslint.config.js der Standard und ersetzt die verschachtelte .eslintrc-Vererbung durch ein einfaches Array von Konfigurationsobjekten, die von oben nach unten zusammengefuehrt werden. Fuer TypeScript-Projekte bietet das Paket typescript-eslint den Helfer tseslint.config(), der Typinferenz fuer die Konfiguration liefert und mehrere vorgefertigte Regel-Presets exportiert: tseslint.configs.recommended fuer reine Syntax-Regeln und tseslint.configs.recommendedTypeChecked fuer die type-aware Variante, die zusaetzlich das vollstaendige TypeScript-Programm benoetigt.

Der entscheidende Konfigurationspunkt fuer type-aware Regeln ist languageOptions.parserOptions.project, das auf die passende tsconfig.json zeigt. Erst dadurch kann der Parser einen echten TypeScript-Program mit vollstaendiger Typinformation aufbauen, den die type-aware Regeln anschliessend abfragen. Ohne diesen Eintrag laufen nur die syntaktischen Regeln, type-aware Regeln werfen dagegen einen Konfigurationsfehler oder werden stillschweigend uebersprungen, je nach ESLint-Version.


// eslint.config.js - flat config using the typescript-eslint helper
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';

export default tseslint.config(
  // Ignore build output and generated files globally
  { ignores: ['dist/**', 'coverage/**', '**/*.generated.ts'] },

  // Type-aware recommended rules, applied only to source files
  ...tseslint.configs.recommendedTypeChecked,

  {
    files: ['src/**/*.ts', 'src/**/*.tsx'],
    languageOptions: {
      parserOptions: {
        // Points at the project's tsconfig so the full TS program is built
        project: './tsconfig.json',
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      // Project-specific overrides layered on top of the preset
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/no-floating-promises': 'error',
    },
  },

  // Must be last: turns off formatting-related rules that conflict with Prettier
  eslintConfigPrettier,
);

Wichtig ist die Reihenfolge im Array: Presets werden zuerst eingebunden, projektspezifische Overrides danach, und eslintConfigPrettier steht immer ganz am Ende, damit es zuverlaessig alle vorher aktivierten Formatierungsregeln deaktiviert. Fuer reine JavaScript-Dateien im selben Repository sollte files: ['**/*.ts', '**/*.tsx'] die type-aware Blocks gezielt einschraenken, sonst versucht ESLint, auch .js-Dateien durch den TypeScript-Compiler zu jagen, was in gemischten Projekten zu unnoetigen Fehlern fuehrt.

3. Type-aware Regeln vs. Syntax-Regeln und ihre Kosten

Syntax-Regeln arbeiten ausschliesslich auf dem AST einer einzelnen Datei und kennen keine Typinformation aus anderen Dateien. Sie erkennen zum Beispiel unbenutzte Variablen, falsche Einrueckung oder verbotene Syntaxmuster, aber nicht, ob eine Funktion tatsaechlich mit dem korrekten Typ aufgerufen wird. Type-aware Regeln hingegen fragen den vollstaendigen TypeScript-TypeChecker ab und koennen Fehler erkennen, die erst durch Typinferenz sichtbar werden: no-unsafe-assignment meldet Zuweisungen von any-Werten an typisierte Variablen, no-floating-promises erkennt ignorierte Promises, die weder await-et noch explizit mit .catch() behandelt werden, und restrict-template-expressions verhindert, dass Objekte ohne sinnvolle toString()-Implementierung in Template-Literalen landen.

Dieser Mehrwert hat einen realen Preis: Type-aware Regeln benoetigen fuer jede gelintete Datei ein vollstaendig aufgebautes TypeScript-Programm inklusive aller importierten Module, was in grossen Repositories den Lint-Lauf um den Faktor fuenf bis zehn verlangsamen kann, verglichen mit reinen Syntax-Regeln. Der Grund liegt darin, dass der TypeScript-Compiler nicht nur die aktuelle Datei parst, sondern transitiv alle Abhaengigkeiten aufloest und typprueft, aehnlich wie tsc selbst. Wer type-aware Linting unreflektiert auf ein gesamtes Monorepo anwendet, riskiert Lint-Laeufe, die laenger dauern als der eigentliche Build.


// Flagged by @typescript-eslint/no-floating-promises (type-aware rule)
async function syncInventory(productId: string): Promise<void> {
  // ...
}

function handleProductUpdate(productId: string): void {
  // WRONG: the returned promise is never awaited or handled,
  // a rejection here would be a silent, unhandled error
  syncInventory(productId);
}

// FIX: either await the call...
async function handleProductUpdateFixed(productId: string): Promise<void> {
  await syncInventory(productId);
}

// ...or explicitly mark it as intentionally not awaited
function handleProductUpdateVoid(productId: string): void {
  void syncInventory(productId).catch((error: unknown) => {
    console.error('Inventory sync failed', error);
  });
}

4. Performance im Griff behalten: Scoping und Caching

Die wirksamste Massnahme gegen lange Lint-Laufzeiten ist das gezielte Scoping von parserOptions.project. Statt ein einziges monolithisches tsconfig.json fuer Quellcode, Tests und Konfigurationsdateien zu verwenden, empfiehlt sich eine separate, schlankere tsconfig.eslint.json, die nur die tatsaechlich zu lintenden Dateien einschliesst und unnoetige Type-Definitionen fuer Build-Tools aussen vor laesst. Dadurch baut der TypeScript-Compiler ein kleineres Programm auf, was die Analysezeit spuerbar reduziert, ohne die Aussagekraft der type-aware Regeln fuer den eigentlichen Anwendungscode einzuschraenken.

Eine zweite wirksame Massnahme ist, type-aware Regeln nicht auf jeden lokalen Speichervorgang anzuwenden, sondern nur auf gezielte Trigger wie Pre-Commit-Hooks oder die CI-Pipeline. ESLint selbst unterstuetzt seit Version 9 eine eingebaute Ergebnis-Caching-Funktion ueber --cache und --cache-location, die bei wiederholten Laeufen nur geaenderte Dateien neu analysiert. In Monorepos mit TypeScript-Projektreferenzen (references in tsconfig.json) lohnt es sich zusaetzlich, pro Package eine eigene, kleine ESLint-Konfiguration mit eigenem project-Pfad zu pflegen, statt eine einzige Root-Konfiguration ueber alle Packages hinweg laufen zu lassen.

5. Eine sinnvolle Regel-Baseline fuer echte Projekte

Beim Einstieg in ein neues oder bestehendes TypeScript-Projekt lohnt es sich, nicht sofort alle verfuegbaren Regeln auf error zu stellen. Eine bewaehrte Baseline aktiviert zuerst die Regeln mit dem groessten Sicherheitsgewinn: no-explicit-any zwingt zu bewusster Typisierung statt stillem Escape-Hatch, no-floating-promises verhindert unbemerkt verschluckte Fehler in asynchronem Code, consistent-type-imports trennt reine Typ-Importe konsequent von Werte-Importen und verbessert damit Tree-Shaking, und die TS-bewusste Variante von no-unused-vars erkennt ungenutzte Type-Parameter, die das eingebaute ESLint-Regelwerk uebersieht.

Andere Regeln wie naming-convention oder no-magic-numbers erzeugen in bestehenden Codebasen oft hunderte Erstmeldungen und sollten anfangs entweder als warn statt error laufen oder gezielt mit gelockerten Optionen konfiguriert werden, damit das Team sich nicht sofort von der Anzahl der Findings entmutigen laesst. Sinnvoll ist es, neue Regeln schrittweise von warn auf error zu heben, sobald der bestehende Code aufgeraeumt ist, statt alles auf einmal einzufuehren.


// Baseline rule severities with reasoning for a real-world TypeScript project
export const baselineRules = {
  // High-value rules: turn on early, catch real bugs
  '@typescript-eslint/no-explicit-any': 'error',
  '@typescript-eslint/no-floating-promises': 'error',
  '@typescript-eslint/consistent-type-imports': 'error',
  '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],

  // Noisy in existing codebases: start as warnings, tighten later
  '@typescript-eslint/naming-convention': 'off',
  '@typescript-eslint/no-magic-numbers': 'off',

  // Type-aware but genuinely worth the runtime cost
  '@typescript-eslint/no-unsafe-assignment': 'warn',
  '@typescript-eslint/restrict-template-expressions': 'warn',

  // Formatting rules: disabled entirely, handled by Prettier instead
  '@typescript-eslint/indent': 'off',
};

6. Formatierungskonflikte mit eslint-config-prettier vermeiden

Ein haeufiges Missverstaendnis ist der Versuch, Formatierung ueber ESLint-Regeln wie indent oder quotes durchzusetzen, waehrend gleichzeitig Prettier im Projekt laeuft. Beide Tools verfolgen unterschiedliche Philosophien: Prettier trifft Formatierungsentscheidungen deterministisch und ohne Konfigurationsspielraum, ESLint-Formatierungsregeln sind dagegen konfigurierbar und kollidieren fast zwangslaeufig mit Prettiers Vorgaben, sobald beide gleichzeitig aktiv sind. Das Ergebnis sind widerspruechliche Fehlermeldungen, bei denen ESLint eine Formatierung verlangt, die Prettier im selben Moment wieder rueckgaengig macht.

Die etablierte Loesung ist eslint-config-prettier, ein Konfigurationspaket, das keine eigenen Regeln definiert, sondern gezielt alle Formatierungsregeln deaktiviert, die mit Prettier kollidieren koennten. Es muss als letztes Element in der Flat-Config-Kette stehen, damit es zuverlaessig alle zuvor aktivierten Konflikt-Regeln ueberschreibt. Prettier selbst wird separat als eigener Formatierungsschritt ausgefuehrt, meist ueber einen Pre-Commit-Hook oder ein IDE-Plugin, und ESLint konzentriert sich dadurch ausschliesslich auf inhaltliche Codequalitaet statt auf Whitespace und Anfuehrungszeichen.

7. package.json-Skripte: lint, lint:ci und typecheck trennen

Ein produktives Setup trennt drei unterschiedliche Pruefstufen in eigene npm-Skripte, statt alles in einem einzigen langsamen Befehl zu buendeln. Ein schnelles lint-Skript ohne type-aware Regeln laeuft in Sekunden und eignet sich fuer den Pre-Commit-Hook oder den Watch-Modus im Editor. Ein separates lint:type-aware-Skript aktiviert die vollen type-aware Regeln und laeuft typischerweise nur in der CI-Pipeline oder gezielt vor einem Release, wo die laengere Laufzeit weniger stoert. Ein eigenstaendiges typecheck-Skript mit tsc --noEmit deckt schliesslich alle Typfehler ab, die ESLint gar nicht erst prueft, etwa fehlende Rueckgabewerte oder inkompatible Funktionssignaturen ueber Modulgrenzen hinweg.

Diese Trennung verhindert, dass Entwickler im lokalen Alltag auf einen langsamen type-aware Lint-Lauf warten muessen, waehrend die CI-Pipeline trotzdem die volle Pruefungstiefe bekommt. Wichtig ist, lint:type-aware und typecheck nicht redundant zu betreiben: tsc --noEmit prueft Typkorrektheit vollstaendig und schnell ueber den nativen Compiler, waehrend type-aware ESLint-Regeln zusaetzliche Stilfragen und Best-Practice-Verstoesse aufdecken, die tsc selbst nicht als Fehler behandelt.


{
  "scripts": {
    "lint": "eslint . --max-warnings=0",
    "lint:type-aware": "eslint . --config eslint.config.type-aware.js",
    "typecheck": "tsc --noEmit -p tsconfig.json",
    "ci": "npm run typecheck && npm run lint:type-aware"
  }
}

8. Integration mit tsc in der CI ohne doppelte Arbeit

In der CI-Pipeline lassen sich tsc --noEmit und ESLint parallel statt sequenziell ausfuehren, da beide unabhaengig voneinander arbeiten und keine gemeinsamen Artefakte teilen. tsc --noEmit uebernimmt die vollstaendige, autoritative Typpruefung ueber das gesamte Projekt inklusive aller Modulgrenzen, waehrend ESLint parallel mit einer engeren, moeglicherweise nicht-type-aware Konfiguration Stilverstoesse und einfache Bugs findet. Ein haeufiger Fehler ist, type-aware ESLint-Regeln als Ersatz fuer tsc zu betrachten: ESLint prueft Dateien einzeln und ist fuer manche komplexen Typfehler, etwa zirkulaere Generic-Constraints, schlicht nicht das richtige Werkzeug.

Caching reduziert die CI-Laufzeit zusaetzlich erheblich. Der --cache-Flag von ESLint kombiniert mit einem persistenten Cache-Verzeichnis zwischen CI-Laeufen spart bei unveraenderten Dateien die komplette Neuanalyse. Fuer TypeScript selbst uebernimmt tsc --build mit Projektreferenzen eine aehnliche Rolle in Monorepos: Nur Packages mit tatsaechlichen Aenderungen werden neu typgeprueft, unveraenderte Packages liefern ihr zwischengespeichertes Ergebnis direkt zurueck. Beide Caching-Mechanismen zusammen reduzieren die CI-Laufzeit in grossen Repositories oft um mehr als die Haelfte.


# .github/workflows/ci.yml - lint and typecheck run in parallel with caching
name: CI

on: [push, pull_request]

jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run typecheck

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      # Restore ESLint's own result cache between runs
      - uses: actions/cache@v4
        with:
          path: .eslintcache
          key: eslint-${{ hashFiles('**/*.ts', '**/*.tsx') }}
      - run: npm run lint:type-aware -- --cache --cache-location .eslintcache

9. Naives Setup vs. empfohlenes Setup im Vergleich

Viele Teams uebernehmen ein ESLint-TypeScript-Setup aus einem Tutorial oder Boilerplate-Repo, ohne die Konsequenzen fuer Performance und Fehlerabdeckung zu hinterfragen. Die folgende Tabelle stellt typische Fehlkonfigurationen den empfohlenen Loesungen gegenueber.

Aspekt Naives Setup Empfohlenes Setup Nutzen
Parser-Konfiguration Kein Parser, ESLint bricht bei TS-Syntax ab typescript-eslint mit tseslint.config() TS-Syntax wird ueberhaupt geparst
Type-aware Regeln recommendedTypeChecked ueberall, auch lokal Type-aware nur in CI/Pre-Commit, scoped project Lint bleibt lokal schnell
Formatierung ESLint indent/quotes parallel zu Prettier eslint-config-prettier als letztes Element Keine widerspruechlichen Fehler
tsc vs. ESLint Nur ESLint, tsc --noEmit fehlt in CI Beide parallel als eigene CI-Jobs Vollstaendige Typabdeckung ueber Modulgrenzen
Caching Kein --cache, jeder Lauf komplett neu --cache plus persistenter CI-Cache Deutlich kuerzere CI-Laufzeit

Der gemeinsame Nenner der naiven Fehler ist fast immer derselbe: Type-aware Linting wird ungefiltert ueberall angewendet, statt gezielt dort, wo es den groessten Nutzen bei vertretbarer Laufzeit bringt. Wer die fuenf Punkte aus der Tabelle konsequent umsetzt, bekommt ein ESLint-Setup, das sowohl im lokalen Editor als auch in der CI-Pipeline zuverlaessig und schnell bleibt.

Mironsoft

TypeScript-Tooling, ESLint-Konfiguration und CI-Pipelines fuer Frontend-Teams

ESLint-Setup, das nicht ausbremst?

Wir analysieren euer bestehendes ESLint- und TypeScript-Setup, identifizieren unnoetig langsame type-aware Regeln und bauen eine klare Trennung zwischen schnellem lokalem Lint und vollstaendiger CI-Pruefung.

Flat-Config-Migration

Umstieg von .eslintrc auf eslint.config.js mit typescript-eslint

Performance-Tuning

Scoping von parserOptions.project und Caching-Strategien fuer schnelle CI

Regel-Baseline

Sinnvolle Regel-Set-Definition ohne hunderte Erstmeldungen im Bestandscode

10. Zusammenfassung

Ein sauber konfiguriertes ESLint-Setup fuer TypeScript beginnt mit dem Paket typescript-eslint, das Parser und Regeln bereitstellt, die den TypeScript-AST tatsaechlich verstehen. Die moderne Flat Config mit tseslint.config() und parserOptions.project baut ein vollstaendiges TypeScript-Programm auf, das type-aware Regeln wie no-floating-promises oder no-unsafe-assignment erst moeglich macht. Diese Regeln finden echte Bugs, die reine Syntax-Regeln nicht erkennen koennen, kosten aber messbar Laufzeit, weil sie den vollen TypeScript-Compiler benoetigen.

Die praktische Loesung liegt in der Trennung: ein schnelles, nicht-type-aware Lint fuer den lokalen Alltag und Pre-Commit-Hooks, ein type-aware Lint plus tsc --noEmit als zwei parallele, gecachte Jobs in der CI-Pipeline. Eine schrittweise eingefuehrte Regel-Baseline und eslint-config-prettier gegen Formatierungskonflikte runden ein Setup ab, das sowohl echte Fehler zuverlaessig findet als auch im Entwickleralltag nicht ausbremst.

ESLint fuer TypeScript richtig konfigurieren - Das Wichtigste auf einen Blick

Parser und Plugin

typescript-eslint buendelt Parser und Regeln, die den TypeScript-AST verstehen. Ohne sie bricht ESLint bei TS-Syntax ab.

Type-aware Regeln

no-floating-promises und no-unsafe-assignment finden echte Bugs, kosten aber 5-10x mehr Laufzeit als Syntax-Regeln.

Performance-Scoping

Eigene tsconfig.eslint.json, --cache und type-aware Regeln nur in CI statt bei jedem lokalen Speichern.

CI-Integration

tsc --noEmit und ESLint parallel als eigene Jobs, keine Redundanz, beide mit Caching-Strategie.

11. FAQ: ESLint fuer TypeScript richtig konfigurieren

1Warum kann ESLint TypeScript-Dateien nicht ohne zusaetzliches Paket lesen?
Der Standard-Parser Espree versteht nur JavaScript-Syntax. Interfaces, Generics und Type-Annotationen existieren dort nicht, daher bricht das Parsing ohne @typescript-eslint/parser sofort ab.
2Was ist der Unterschied zwischen typescript-eslint und den Einzelpaketen?
typescript-eslint buendelt Parser, Plugin und Konfigurations-Helfer wie tseslint.config() aus einer Hand und vermeidet Versionskonflikte. Fuer neue Projekte der empfohlene Einstieg.
3Was macht eine Regel type-aware im Gegensatz zu Syntax-Regeln?
Type-aware Regeln fragen den vollstaendigen TypeChecker ab und kennen Typinformation ueber Dateigrenzen hinweg. Syntax-Regeln arbeiten nur auf dem AST einer einzelnen Datei.
4Wie stark verlangsamen type-aware Regeln den Lint-Lauf?
In grossen Repos oft um Faktor 5 bis 10, weil fuer jede Datei ein vollstaendiges TypeScript-Programm inklusive aller Abhaengigkeiten aufgebaut werden muss.
5Was bewirkt parserOptions.project konkret?
Zeigt auf die tsconfig.json und ermoeglicht dem Parser, ein echtes TypeScript-Programm mit voller Typinformation aufzubauen, das type-aware Regeln erst funktionsfaehig macht.
6Sollte ich alle Regeln von Anfang an auf error stellen?
Nein. Hochwertige Regeln frueh aktivieren, noisy Regeln wie naming-convention anfangs als warn laufen lassen, um bestehenden Code nicht sofort zu ueberfluten.
7Warum braucht man eslint-config-prettier zusaetzlich zu Prettier?
Es deaktiviert alle ESLint-Formatierungsregeln, die mit Prettiers Entscheidungen kollidieren koennten. Ohne dieses Paket melden beide Tools teils widerspruechliche Fehler.
8Ersetzt type-aware ESLint-Linting den Aufruf von tsc --noEmit?
Nein. tsc --noEmit bleibt die autoritative, vollstaendige Typpruefung. ESLint findet zusaetzliche Stilfragen, ersetzt aber nicht die Korrektheitspruefung des Compilers.
9Wie sollte ESLint und tsc in der CI kombiniert werden?
Als zwei parallele Jobs statt sequenziell: ein Job fuer tsc --noEmit, ein Job fuer type-aware ESLint mit --cache. Beide pruefen unterschiedliche Dinge.
10Wie reduziere ich die Lint-Laufzeit in einem Monorepo?
Eigene schlanke tsconfig.eslint.json pro Package, ESLints --cache ueber CI-Laeufe persistieren, TypeScript-Projektreferenzen nutzen, damit tsc --build nur veraenderte Packages neu prueft.