Einleitung: Warum Legacy-Code in TypeScript einen besonderen Ansatz erfordert
Die Arbeit mit vererbtem (Legacy-)Code ist für jeden Entwickler eine unvermeidliche Realität, insbesondere in großen Projekten. Oft wurde solcher Code ohne strenge Typisierung geschrieben, mit any an jeder Ecke, und stellt ein verworrenes Geflecht von Abhängigkeiten dar. Einfaches Refactoring funktioniert hier nicht – es braucht einen systematischen, fortgeschrittenen Ansatz.
In diesem Artikel betrachten wir Strategien zum Refactoring von Legacy-Code in TypeScript, die über grundlegende Anleitungen hinausgehen. Sie erfahren, wie Sie sicher strenge Typisierung einführen, Änderungen mit Hilfe von Entwurfsmustern isolieren und die Möglichkeiten des Compilers zur Automatisierung von Routineaufgaben nutzen.
Strategie 1: Schrittweise Einführung strenger Typisierung durch "Typ-Brücken"
Eines der Hauptprobleme von Legacy-Code ist die totale Verwendung von any. Der Versuch, alle any auf einmal durch konkrete Typen zu ersetzen, führt zu Hunderten von Fehlern und einer Lähmung der Entwicklung. Stattdessen verwenden wir die Technik der "Typ-Brücken".
Der Kern der Methode: Wir schreiben nicht das gesamte Modul auf einmal um, sondern erstellen Zwischentypen als Brücken, die nach und nach das any verdrängen.
Beispiel: Typisierung eines "grauen" Moduls
Angenommen, wir haben ein altes Modul für die Benutzerverwaltung:
// legacyModule.tsexport function processUser(data: any): any { // ... viel unlesbarer Code return { id: data.id, name: data.name, age: data.age };}Anstatt die gesamte Funktion sofort zu typisieren, erstellen wir UserData und User:
// types.tsexport interface UserData { id: number; name: string; age?: number; // Feld kann fehlen}
export interface User { id: number; fullName: string; isAdult: boolean;}Jetzt verwenden wir diese Typen, behalten aber die alte Signatur aus Gründen der Abwärtskompatibilität bei:
// legacyModule.ts (Refactoring Schritt 1)import { UserData, User } from './types';
export function processUser(data: UserData): User { const isAdult = data.age ? data.age >= 18 : false; return { id: data.id, fullName: data.name, isAdult, };}
// Alte Signatur für Kompatibilitätexport function processUserLegacy(data: any): any { return processUser(data as UserData);}Nachdem alle Konsumenten auf die neue Funktion umgestiegen sind, löschen wir processUserLegacy. Dieser Ansatz minimiert das Risiko, Abhängigkeiten zu beschädigen.
Strategie 2: Isolierung "schmutziger" Abhängigkeiten mit dem "Adapter"-Muster
Legacy-Code ist oft eng mit externen Bibliotheken, globalen Zuständen oder veralteten APIs verbunden. Um ein sicheres Refactoring durchzuführen, müssen diese Abhängigkeiten isoliert werden. Das beste Werkzeug dafür ist das "Adapter"-Muster in Kombination mit Interfaces.
Beispiel: Isolierung einer veralteten API
// oldApi.ts (Legacy)export function fetchOldData(callback: (data: any) => void): void { // alte API mit Callbacks setTimeout(() => callback({ value: 42 }), 100);}Wir erstellen ein Interface für den neuen Ansatz (Promise-basiert) und einen Adapter:
// apiAdapter.tsimport { fetchOldData } from './oldApi';
export interface IDataFetcher { fetchData(): Promise<{ value: number }>;}
export class LegacyApiAdapter implements IDataFetcher { fetchData(): Promise<{ value: number }> { return new Promise((resolve, reject) => { fetchOldData((data) => { if (data) { resolve(data); } else { reject(new Error('No data')); } }); }); }}Jetzt verwendet der gesamte neue Code IDataFetcher. Wenn Sie die alte API vollständig umschreiben, müssen Sie nur die Implementierung des Adapters austauschen, ohne den restlichen Code zu berühren. Dies reduziert das Risiko von Regressionen auf ein Minimum.