Zum Inhalt springen

Schrittweise Migration von JavaScript zu TypeScript

Schrittweise Migration bestehender JavaScript-Projekte zu TypeScript ohne Feature-Stopp: Konfiguration, Strict-Modus und Team-Workflows.

4 Min. Lesezeit
Migrations-Zeitleiste, die zeigt, wie JavaScript-Dateien schrittweise über verschiedene Strict-Mode-Stufen zu TypeScript konvertiert werden

Die Migration einer JavaScript-Codebasis zu TypeScript erfordert keinen radikalen Komplettumbau. Die erfolgreichsten Migrationen laufen schrittweise ab — eine Datei nach der anderen —, wobei jeder Schritt zusätzliche Typsicherheit bringt und die Anwendung dabei funktionsfähig bleibt. Neue Features lassen sich schon während der Migration ausliefern, nicht erst danach.

Die entscheidende Erkenntnis: TypeScript ist eine Obermenge von JavaScript. Jede .js-Datei ist mit minimalen Änderungen gültiges TypeScript. Beginne permissiv und verschärfe die Strenge, sobald die Abdeckung wächst.

Phase 1: Konfiguration ohne Codeänderungen

Bevor du auch nur eine JavaScript-Datei anfasst, richte TypeScript so ein, dass es deine bestehende Codebasis unverändert akzeptiert.

jsonjson
// tsconfig.json — permissive starting point
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "allowJs": true,          // Allow .js files alongside .ts
    "checkJs": false,         // Don't type-check .js files yet
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": false,          // Start permissive
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
shbash
# Install TypeScript and type definitions for your dependencies
npm install -D typescript @types/node @types/react @types/express
# Check which @types packages exist for your dependencies
npx typesync

Zu diesem Zeitpunkt ändert sich am bestehenden Code nichts. JavaScript-Dateien werden von TypeScript unverändert durchkompiliert. Der Build funktioniert. Die Tests laufen durch. Deploye es.

Phase 2: Dateien gezielt umbenennen

Beginne damit, .js-Dateien in .ts umzubenennen (bzw. .jsx in .tsx), und zwar zuerst bei Blattmodulen — Dateien, die andere importieren, selbst aber kaum von anderen importiert werden.

## Migration order (leaf → root):

1. Utility functions (src/utils/*.js → *.ts)
   - Pure functions, few dependencies, easy to type

2. Data types and constants (src/types/*.js → *.ts)
   - Define interfaces that other files will use

3. Service layers (src/services/*.js → *.ts)
   - Database queries, API clients

4. Business logic (src/domain/*.js → *.ts)
   - Core application logic

5. Route handlers / components (src/routes/*.js → *.ts)
   - Entry points that compose everything

6. Entry point (src/index.js → src/index.ts)
   - Last file to convert
tstypescript
// ❌ Converting a file and adding full types immediately
// This blocks the migration because you need types for everything
 
// ✅ Rename and add minimal types — fix the rest incrementally
// Before: src/utils/format.js
export function formatCurrency(amount) {
  return `$${amount.toFixed(2)}`;
}
 
// After: src/utils/format.ts
export function formatCurrency(amount: number): string {
  return `$${amount.toFixed(2)}`;
}
// Quick win — function signature typed, behavior unchanged

Phase 3: Zentrale Interfaces definieren

Lege frühzeitig Typdefinitionen für deine Domänenobjekte an. Diese Interfaces werden von jeder Datei genutzt, die du danach konvertierst.

tstypescript
// src/types/domain.ts
export interface User {
  id: string;
  email: string;
  name: string;
  role: 'admin' | 'user' | 'moderator';
  createdAt: Date;
  updatedAt: Date;
}
 
export interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
  inStock: boolean;
}
 
export interface Order {
  id: string;
  userId: string;
  items: OrderItem[];
  total: number;
  status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled';
  createdAt: Date;
}
 
export interface OrderItem {
  productId: string;
  quantity: number;
  unitPrice: number;
}
 
// API response wrapper
export interface ApiResponse<T> {
  data: T;
  meta?: {
    page: number;
    pageSize: number;
    total: number;
  };
}
 
export interface ApiError {
  error: string;
  status: number;
  details?: Record<string, string[]>;
}

Phase 4: Umgang mit Drittanbieter-Bibliotheken

Manche Bibliotheken haben eigene @types-Pakete. Manche bringen ihre Typen schon mit. Manche haben weder noch. Jeder Fall wird anders behandelt.

tstypescript
// Case 1: Library has @types package — just install it
// npm install -D @types/lodash
import { groupBy } from 'lodash';
// Types work automatically
 
// Case 2: Library has built-in types — nothing to install
import axios from 'axios';
// axios ships its own .d.ts files
 
// Case 3: No types available — create a declaration file
// src/types/untyped-lib.d.ts
declare module 'legacy-csv-parser' {
  interface ParseOptions {
    delimiter?: string;
    headers?: boolean;
  }
 
  interface ParseResult {
    data: Record<string, string>[];
    errors: string[];
  }
 
  export function parse(input: string, options?: ParseOptions): ParseResult;
}
 
// Now you can import with types:
import { parse } from 'legacy-csv-parser';
const result = parse(csvData, { delimiter: ',' });
// result.data is Record<string, string>[]
tstypescript
// ❌ Using 'any' to silence type errors on untyped libraries
const result: any = legacyLib.doSomething();
// Loses all type safety downstream
 
// ✅ Create a minimal declaration and improve it over time
// Start with what you actually use, not the full API surface
declare module 'legacy-lib' {
  export function doSomething(): { id: string; value: number };
}

Phase 5: Schrittweiser Strict-Modus

Das strict-Flag von TypeScript ist eigentlich ein Bündel einzelner Prüfungen. Aktiviere sie nacheinander statt alle auf einmal.

jsonjson
// ❌ Enabling strict all at once — hundreds of errors
{
  "compilerOptions": {
    "strict": true  // Enables ALL strict checks simultaneously
  }
}
jsonjson
// ✅ Enable individual strict checks progressively
{
  "compilerOptions": {
    "strict": false,
 
    // Phase 1: Enable these first (easiest to fix)
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,
 
    // Phase 2: Enable after Phase 1 is clean
    "noImplicitAny": true,           // Most impactful
    "strictNullChecks": true,         // Catches real bugs
 
    // Phase 3: Enable last (most strict)
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true
  }
}
tstypescript
// noImplicitAny — before:
function processItem(item) {    // 'item' implicitly has 'any' type
  return item.name.toUpperCase();
}
 
// noImplicitAny — after:
function processItem(item: Product): string {
  return item.name.toUpperCase();
}
 
// strictNullChecks — before:
function getUser(id: string): User {
  return users.find(u => u.id === id);  // Could be undefined!
}
 
// strictNullChecks — after:
function getUser(id: string): User | undefined {
  return users.find(u => u.id === id);
}
 
// Callers must handle the undefined case:
const user = getUser('123');
if (!user) {
  throw new Error('User not found');
}
console.log(user.name); // TypeScript knows user is not undefined here

strictNullChecks ist für sich genommen die wertvollste Option. Sie fängt null/undefined-Fehler schon beim Kompilieren ab, die sonst zu Abstürzen zur Laufzeit führen würden.

Migrationsfortschritt verfolgen

Miss den Fortschritt, damit das Team motiviert bleibt und das Management den Wert der Investition versteht.

shbash
#!/bin/bash
# scripts/migration-progress.sh
 
TOTAL_JS=$(find src -name "*.js" -o -name "*.jsx" | wc -l)
TOTAL_TS=$(find src -name "*.ts" -o -name "*.tsx" | wc -l)
TOTAL=$((TOTAL_JS + TOTAL_TS))
 
if [ $TOTAL -gt 0 ]; then
  PERCENT=$((TOTAL_TS * 100 / TOTAL))
else
  PERCENT=0
fi
 
echo "Migration Progress:"
echo "  JavaScript files: $TOTAL_JS"
echo "  TypeScript files: $TOTAL_TS"
echo "  Total: $TOTAL"
echo "  Progress: ${PERCENT}%"
echo ""
echo "Type coverage:"
npx type-coverage --detail

Führe das in der CI aus und verfolge den Prozentsatz über die Zeit. Eine stetig steigende Linie motiviert. Eine flache Linie bedeutet, dass die Migration ins Stocken geraten ist und Aufmerksamkeit braucht.

Die wichtigsten Erkenntnisse

  1. Beginne mit allowJs: true und strict: false — lass TypeScript das bestehende JavaScript unverändert akzeptieren
  2. Konvertiere zuerst die Blattmodule — Utilities und Typen vor Route-Handlern und Einstiegspunkten
  3. Definiere Domänen-Interfaces frühzeitig — gemeinsam genutzte Typen beschleunigen die Konvertierung jeder weiteren Datei
  4. Aktiviere strikte Prüfungen schrittweise — zuerst noImplicitAny und strictNullChecks, dann den Rest
  5. Erstelle Deklarationsdateien für ungetypte Bibliotheken — nur minimale Typen für das, was du tatsächlich nutzt, nicht die gesamte API
  6. Verfolge den Fortschritt in der CI — miss den Anteil der TypeScript-Dateien und die Typabdeckung, um den Schwung zu erhalten
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX