any schrittweise aus einer bestehenden Codebase eliminieren
<T>
type
TypeScript · any · Type Safety · Migration
any schrittweise aus einer bestehenden Codebase eliminieren
Von noImplicitAny bis type-coverage: der praktische Migrationspfad

Wer any unkontrolliert im Code verteilt, hebelt die Typprüfung genau an den Stellen aus, an denen sie am wichtigsten wäre. Dieser Artikel zeigt den praktischen Weg zu einer typsicheren Codebase: noImplicitAny gezielt aktivieren, ESLint-Regeln schrittweise verschärfen, unknown und Generics als Ersatz nutzen und den Fortschritt mit type-coverage messbar machen, ohne die laufende Entwicklung zu blockieren.

14 Min. Lesezeit noImplicitAny · ESLint-Ratchet · type-coverage TypeScript 5.x · Legacy-Migration

1. Warum any den Type-Checker lautlos aushebelt

any ist in TypeScript kein Typ im eigentlichen Sinn, sondern eine Notausstiegsklappe, die den kompletten Typ-Checker für eine Variable, einen Parameter oder einen Rückgabewert abschaltet. Der Compiler prüft an dieser Stelle nichts mehr: weder Property-Zugriffe, noch Methodenaufrufe, noch die Kompatibilität mit anderen Typen. Das eigentliche Problem zeigt sich aber nicht an der Stelle, an der any hingeschrieben wurde, sondern an jeder Stelle, die diesen Wert später weiterverwendet. Ein einzelnes any in einer tief verschachtelten Utility-Funktion reicht aus, um die Sicherheit eines ganzen Aufrufbaums zu untergraben.

Durch TypeScripts Typinferenz breitet sich any wie ein Fleck aus. Wird der Rückgabewert einer Funktion nicht explizit typisiert und enthält intern any, inferiert der Compiler automatisch any als Rückgabetyp - jeder Aufrufer erbt diese Lücke, ohne eine Warnung zu sehen. Bei generischen Funktionen ist der Effekt noch gravierender: Wird ein generischer Parameter mit any instanziiert, kollabiert die gesamte Typprüfung innerhalb dieser Instanz zu any, selbst wenn die Funktion selbst sauber typisiert ist. In gewachsenen Codebases mit hunderten Modulen genügen wenige solcher Wurzeln, um große Teile der Anwendung effektiv ungetypt zu machen.

2. noImplicitAny schrittweise aktivieren

Das Compiler-Flag noImplicitAny ist die Grundvoraussetzung für eine typsichere Codebase: Es zwingt TypeScript, jeden Parameter, jede Variable und jeden Rückgabewert ohne erkennbaren Typ als Fehler statt als stillschweigendes any zu behandeln. In einer frisch aufgesetzten Codebase aktiviert man das Flag einfach in der tsconfig.json. In einer gewachsenen Legacy-Codebase mit tausenden Dateien führt das sofortige Aktivieren aber zu hunderten Fehlern auf einen Schlag - ein Zustand, den kein Team in einem Rutsch beheben kann, ohne die laufende Feature-Entwicklung zu blockieren.

Der praktikable Weg ist eine schrittweise Migration über eine zweite tsconfig-Datei, die noImplicitAny nur für eine explizite Liste bereits geprüfter Verzeichnisse aktiviert und von der Basis-Konfiguration erbt. Neue Dateien werden dieser Liste hinzugefügt, sobald sie sauber typisiert sind; CI prüft beide Konfigurationen parallel. Die sinnvolle Migrationsreihenfolge beginnt bei Leaf-Modulen und Utility-Funktionen ohne eigene Abhängigkeiten, da sie am schnellsten fehlerfrei werden und als stabile Basis für alle Consumer-Module dienen. Erst wenn diese Fundamentschicht sauber ist, lohnt sich die Migration der aufrufenden Komponenten und Services, die von ihr abhängen.


// tsconfig.json - base config, still permissive for the whole codebase
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "strict": false,
    "noImplicitAny": false,
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] }
  },
  "include": ["src"]
}

// tsconfig.strict.json - opt-in noImplicitAny for already migrated files only
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true
  },
  "include": [
    "src/utils/**/*.ts",
    "src/lib/format-currency.ts",
    "src/lib/parse-api-response.ts"
  ]
}

// package.json - run both checks in CI, strict config only covers migrated files
// "scripts": { "typecheck": "tsc --noEmit", "typecheck:strict": "tsc -p tsconfig.strict.json --noEmit" }

3. ESLint-Strategie: no-explicit-any von warn zu error

Während noImplicitAny nur implizites any erfasst, deckt die ESLint-Regel @typescript-eslint/no-explicit-any zusätzlich jedes bewusst hingeschriebene any: Type-Assertions, explizite Parametertypen und Rückgabetypen. Der erste Schritt in einer Legacy-Codebase ist nicht, die Regel scharf zu schalten, sondern sie auf warn zu setzen und damit eine ehrliche Baseline zu ermitteln, ohne die CI-Pipeline zu blockieren. Diese Zahl, oft mehrere hundert Vorkommen, ist der Ausgangspunkt für jede weitere Planung.

Mit ESLints overrides-Mechanismus lässt sich die Regel pro Verzeichnis unterschiedlich konfigurieren: Verzeichnisse, die vollständig migriert sind, bekommen error, alles andere bleibt vorerst bei warn. Der entscheidende Hebel ist ein Ratchet-Mechanismus über --max-warnings: Das Budget wird beim aktuellen Warnungs-Stand eingefroren und darf mit jedem Merge nur sinken, nie steigen. Jeder Pull Request, der neue any-Vorkommen einführt, lässt die CI-Pipeline fehlschlagen, während bestehende Warnungen schrittweise durch dedizierte Cleanup-Commits abgebaut werden. So verhindert man ein erneutes Anwachsen, ohne sofort alle Bestandswarnungen beheben zu müssen.


// eslint.config.js - flat config with per-directory ratchet for no-explicit-any
import tseslint from 'typescript-eslint';

export default tseslint.config(
  {
    files: ['**/*.ts', '**/*.tsx'],
    rules: {
      // Baseline: warn everywhere so CI stays green while we measure the count
      '@typescript-eslint/no-explicit-any': 'warn',
    },
  },
  {
    // Fully migrated directories: any is now a hard error
    files: ['src/utils/**/*.ts', 'src/lib/**/*.ts', 'src/api/client.ts'],
    rules: {
      '@typescript-eslint/no-explicit-any': 'error',
    },
  },
  {
    // Legacy directory not yet touched: still warn, tracked separately
    files: ['src/legacy/**/*.ts'],
    rules: {
      '@typescript-eslint/no-explicit-any': 'warn',
    },
  },
);

// CI ratchet: budget only ever goes down, never up
// "lint:any-budget": "eslint . --max-warnings 42"

4. unknown statt any: Typsicherheit für externe Daten

Die häufigste Ausrede für any ist externe, nicht vertrauenswürdige Eingabe: das Ergebnis von JSON.parse, eine API-Antwort oder Nutzereingaben aus einem Formular. In allen diesen Fällen ist unknown der korrekte Typ, nicht any. unknown erlaubt der Variable jeden Wert, verweigert aber jeden Zugriff auf Properties oder Methoden, bis der Typ explizit eingeschränkt wurde. Der Compiler zwingt so zur Auseinandersetzung mit der tatsächlichen Form der Daten, statt sie blind zu vertrauen.

Die Einschränkung erfolgt über eine Type-Guard-Funktion, die mit einem is-Prädikat den Compiler informiert, welcher konkrete Typ nach einer erfolgreichen Prüfung vorliegt. Für API-Antworten prüft die Guard-Funktion typischerweise mit typeof und in, ob die erwarteten Felder vorhanden sind und die richtigen Primitivtypen tragen. Schlägt die Prüfung fehl, wirft die Funktion einen Fehler - der aufrufende Code bleibt danach vollständig typsicher, ohne einen einzigen Cast.


// Type guard narrows unknown to a known shape before use
interface ApiUser {
  id: number;
  email: string;
  isActive: boolean;
}

function isApiUser(value: unknown): value is ApiUser {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof (value as Record<string, unknown>).id === 'number' &&
    typeof (value as Record<string, unknown>).email === 'string' &&
    typeof (value as Record<string, unknown>).isActive === 'boolean'
  );
}

async function fetchUser(id: number): Promise<ApiUser> {
  const response = await fetch(`/api/users/${id}`);
  const data: unknown = await response.json();

  if (!isApiUser(data)) {
    throw new Error('Unexpected API response shape for user');
  }

  // data is now narrowed to ApiUser, no cast needed
  return data;
}

5. Generics statt any[] für wiederverwendbare Funktionen

Reusable Container- und Utility-Funktionen wie groupBy, chunk oder ein einfacher Cache werden oft mit any[] oder any als Parameter- und Rückgabetyp geschrieben, weil sie mit beliebigen Elementtypen arbeiten sollen. Das Ergebnis ist eine Funktion, die zwar universell einsetzbar ist, aber jede Typinformation an der Aufrufstelle verliert: Der Rückgabewert ist any, egal ob ein Array von Strings oder ein Array von Order-Objekten hineingegeben wurde.

Generics lösen genau dieses Problem, ohne die Wiederverwendbarkeit einzuschränken. Ein generischer Typparameter T bindet den Elementtyp an der Aufrufstelle, sodass der Rückgabetyp automatisch korrekt inferiert wird - eine groupBy-Funktion mit generischem Schlüssel- und Elementtyp liefert exakt das erwartete Record<K, T[]>, ohne dass der Aufrufer irgendetwas casten muss. Der Migrationsaufwand ist überschaubar: Meist genügt es, den any-Parametertyp durch einen generischen Typparameter zu ersetzen und die interne Logik unverändert zu lassen, da Generics zur Laufzeit ohnehin keine Rolle spielen.


// Generic groupBy replaces an any-based version and stays fully typed
function groupBy<T, K extends PropertyKey>(
  items: readonly T[],
  keyFn: (item: T) => K,
): Record<K, T[]> {
  const result = {} as Record<K, T[]>;

  for (const item of items) {
    const key = keyFn(item);
    (result[key] ??= []).push(item);
  }

  return result;
}

interface Order {
  id: number;
  status: 'pending' | 'shipped' | 'cancelled';
}

const orders: Order[] = [
  { id: 1, status: 'pending' },
  { id: 2, status: 'shipped' },
  { id: 3, status: 'pending' },
];

// ordersByStatus is inferred as Record<'pending' | 'shipped' | 'cancelled', Order[]>
const ordersByStatus = groupBy(orders, (order) => order.status);

6. Record<string, unknown> statt any für lose Objekte

Für lose strukturierte Objekte, etwa Konfigurationsobjekte, Feature-Flags oder dynamisch zusammengesetzte Optionen, greifen viele Entwickler ebenfalls zu any, weil die genaue Form zur Entwicklungszeit unklar erscheint. Record<string, unknown> ist hier fast immer die bessere Wahl: Der Typ drückt exakt aus, was gemeint ist - ein Objekt mit beliebigen String-Keys, dessen Werte im Einzelfall geprüft werden müssen, bevor sie verwendet werden.

Der praktische Unterschied zu any zeigt sich beim Zugriff: config.someKey ist bei any ein beliebiger Wert ohne jede Prüfung, bei Record<string, unknown> zwingt der Compiler dazu, den Wert vor der Verwendung zu typisieren oder mit einem Type Guard einzuschränken. Für Konfigurationsobjekte mit bekannten Pflichtfeldern lohnt sich oft ein hybrider Ansatz: ein Interface mit den bekannten Feldern kombiniert mit einem Index-Signature-Fallback für optionale Erweiterungen, statt das gesamte Objekt auf unknown zu reduzieren.

7. Function Overloads statt any-Parameter

Polymorphe Funktionen, die je nach Eingabetyp unterschiedliches Verhalten zeigen, etwa ein Formatter, der sowohl Zahlen als auch Datums-Objekte annimmt und je nach Typ unterschiedliche Rückgabewerte liefert, werden oft mit any-Parametern geschrieben, weil eine einzelne Signatur die Vielfalt der Eingaben nicht sauber abbilden kann. Das Ergebnis ist eine Funktion, die zwar alles akzeptiert, aber weder dem Aufrufer noch der Implementierung selbst Typsicherheit bietet.

Function Overloads lösen dieses Problem, indem sie mehrere explizite Signaturen vor der eigentlichen Implementierung deklarieren: eine Signatur pro sinnvoller Eingabe-Rückgabe-Kombination. Der Compiler wählt beim Aufruf automatisch die passende Signatur aus und meldet einen Fehler, wenn keine passt. Die Implementierungssignatur selbst darf breiter sein und intern mit einer Union arbeiten, bleibt aber für den Aufrufer unsichtbar. So bekommt jede Kombination aus Eingabe und Rückgabewert exakte Typen, ohne dass irgendwo im Code ein any auftaucht.

8. Fortschritt messen mit type-coverage

Ohne Messung bleibt jede any-Eliminierung ein Gefühl statt ein Fakt. Das npm-Paket type-coverage zählt für jede Datei, wie viele Identifier einen konkreten Typ tragen und wie viele auf any zurückfallen, und aggregiert daraus einen Prozentwert für die gesamte Codebase. Anders als eine reine Fehleranzahl bildet dieser Wert auch implizite any-Stellen ab, die weder ESLint noch der Compiler in der aktuellen Konfiguration melden.

In der CI-Pipeline lässt sich mit type-coverage --at-least 92 ein Mindestwert erzwingen, der den Build fehlschlagen lässt, sobald die Typabdeckung unter die Schwelle fällt. Der entscheidende Vorteil gegenüber einer reinen Momentaufnahme ist der Trend: Wird der Prozentwert bei jedem Merge in ein Dashboard oder Artefakt geschrieben, wird jede schleichende Verschlechterung sofort sichtbar, lange bevor sie zu einem echten Problem wird. Die Schwelle selbst wird wie das ESLint-Budget regelmäßig angehoben, sobald neue Bereiche der Codebase migriert sind.


# package.json: enforce a minimum type-coverage percentage in CI
# "scripts": { "type-coverage": "type-coverage --at-least 92 --detail" }

$ npm run type-coverage

> type-coverage --at-least 92 --detail

src/legacy/order-export.ts:41:12: any
src/legacy/order-export.ts:58:3: any
src/utils/format-price.ts:9:22: any

3 uncovered identifiers found
9532 / 10285 (92.68%)

# CI gate fails the build only when the percentage drops below --at-least

9. any-Code im direkten Vergleich zur typisierten Alternative

Die folgende Übersicht fasst die häufigsten any-Muster aus der Praxis den jeweils empfohlenen typisierten Alternativen gegenüber. Jede Zeile steht für ein wiederkehrendes Szenario aus einer gewachsenen Codebase, von der API-Antwort bis zum catch-Block, und zeigt, welche Änderung ohne großen Aufwand sofort mehr Sicherheit bringt.

Szenario any-Variante Typisierte Alternative Vorteil
JSON-Antwort parsen JSON.parse(str) as any as unknown + Type Guard Fehlerhafte Form fällt beim Parsen auf
Wiederverwendbare Utility function first(arr: any[]): any function first<T>(arr: T[]): T | undefined Rückgabetyp bleibt an der Aufrufstelle erhalten
Lose Konfiguration config: any config: Record<string, unknown> Zugriff erzwingt Prüfung vor Verwendung
Polymorphe Funktion function format(value: any): string Function Overloads pro Eingabetyp Compiler wählt passende Signatur automatisch
catch-Block catch (e: any) catch (e: unknown) + Narrowing Fehlerbehandlung bricht nicht bei falscher Annahme

Der gemeinsame Nenner aller fünf Zeilen: Keine der typisierten Alternativen verlangt einen radikalen Umbau. unknown statt any bei der Deklaration, ein Type Guard oder Cast an der richtigen Stelle, ein generischer Parameter statt eines konkreten any - jede Änderung ist lokal begrenzt und lässt sich in einem einzelnen Pull Request umsetzen. Genau diese kleinteilige Umsetzbarkeit macht die schrittweise Migration in der Praxis realistisch, während ein kompletter Rewrite in den meisten Teams am fehlenden Zeitbudget scheitert.

Mironsoft

TypeScript-Migration, Type Safety und Tooling für gewachsene Codebases

TypeScript-Codebase professionell entschärfen?

Wir analysieren eure Codebase, ermitteln die tatsächliche any-Quote mit type-coverage und richten eine schrittweise Migration mit ESLint-Ratchet und noImplicitAny-Rollout ein, ohne eure Feature-Entwicklung zu blockieren.

Type-Coverage-Audit

Baseline ermitteln, kritische any-Hotspots priorisieren

ESLint- & tsconfig-Setup

Ratchet-Konfiguration und schrittweiser noImplicitAny-Rollout

Team-Coaching

unknown, Generics und Type Guards im Alltag verankern

10. Zusammenfassung

any schrittweise aus einer bestehenden Codebase eliminieren ist kein einmaliges Projekt, sondern ein kontinuierlicher Prozess mit klaren Etappen: noImplicitAny zuerst für Leaf-Module und Utilities aktivieren, dann Schritt für Schritt auf Consumer-Module ausweiten. ESLint mit @typescript-eslint/no-explicit-any auf warn setzen, eine Baseline ermitteln und die Regel pro Verzeichnis auf error verschärfen, sobald ein Bereich sauber ist. unknown statt any für jede externe Eingabe, Generics statt any[] für wiederverwendbare Funktionen, Record<string, unknown> für lose Objekte und Function Overloads für polymorphe APIs ersetzen die häufigsten any-Fluchtpunkte durch echte Typsicherheit.

Der entscheidende Erfolgsfaktor ist Messbarkeit statt Bauchgefühl. type-coverage liefert eine harte Zahl, die sich in der CI-Pipeline als Mindestschwelle durchsetzen lässt und mit jedem Merge weiter steigen darf, aber nie sinken sollte. In Kombination mit dem ESLint-Ratchet über max-warnings entsteht ein System, das neue any-Vorkommen aktiv verhindert, während die bestehenden schrittweise abgebaut werden, ganz ohne einen riskanten Big-Bang-Rewrite der gesamten Codebase.

any schrittweise eliminieren - Das Wichtigste auf einen Blick

noImplicitAny-Rollout

Über eine zweite tsconfig mit Include-Allowlist schrittweise aktivieren, Leaf-Module zuerst.

ESLint-Ratchet

no-explicit-any zuerst auf warn, dann pro Verzeichnis auf error. max-warnings-Budget sinkt bei jedem Merge.

Typisierte Alternativen

unknown + Type Guards, Generics statt any[], Record<string, unknown>, Function Overloads statt any-Parameter.

Fortschritt messen

type-coverage --at-least als CI-Gate, Trend im Dashboard dokumentieren, Schwelle regelmäßig anheben.

11. FAQ: any schrittweise aus einer bestehenden Codebase eliminieren

1Was macht any in TypeScript so gefährlich?
Es schaltet den Typ-Checker vollständig ab und breitet sich über Typinferenz auf Rückgabetypen, Aufrufer und generische Instanzen aus.
2Was prüft noImplicitAny genau?
Jeden Parameter, jede Variable und jeden Rückgabewert ohne erkennbaren Typ. Meldet implizites any als Fehler statt es stillschweigend zuzulassen.
3Wie aktiviere ich noImplicitAny schrittweise?
Über eine zweite tsconfig mit Include-Allowlist bereits migrierter Verzeichnisse. Leaf-Module und Utilities zuerst, dann abhängige Consumer.
4Was bewirkt @typescript-eslint/no-explicit-any?
Meldet jedes bewusst geschriebene any. Zuerst warn für eine Baseline, dann per overrides pro Verzeichnis auf error verschärft.
5Was ist ein Ratchet bei ESLint-Warnungen?
--max-warnings friert das aktuelle Budget ein. Es darf mit jedem Merge nur sinken, neue any-Vorkommen blockieren die CI zuverlässig.
6Wann sollte ich unknown statt any verwenden?
Bei nicht vertrauenswürdiger externer Eingabe wie JSON.parse-Ergebnissen. Zugriff erst nach expliziter Type-Guard-Prüfung erlaubt.
7Warum Generics statt any[] für Utility-Funktionen?
Ein generischer Typparameter bindet den Elementtyp an der Aufrufstelle. any[] verliert dagegen jede Typinformation.
8Wann ist Record<string, unknown> die richtige Wahl?
Für lose strukturierte Objekte wie Konfigurationen. Der Compiler erzwingt eine Typisierung jedes Werts vor der Verwendung.
9Was bringen Function Overloads gegenüber any-Parametern?
Mehrere explizite Signaturen pro Eingabe-Rückgabe-Kombination. Der Compiler wählt die passende Signatur automatisch aus, ganz ohne any.
10Wie misst type-coverage den Fortschritt?
Zählt typisierte gegenüber any-typisierten Identifiern und aggregiert einen Prozentwert. --at-least erzwingt eine Mindestschwelle in CI.