Praxis

TypeScript Best Practices: Sauberer und wartbarer Code

Bewährte Muster für professionellen TypeScript-Code: Strict Mode, Interface vs. Type, Enums, Naming Conventions und häufige Anti-Patterns vermeiden.

Lesezeit 8 Min. Aktualisiert 28.05.2026 1 Quellen Jan-Tristan Rudat Jan-Tristan Rudat
Inhalt

TypeScript Best Practices: Sauberer und wartbarer Code

TypeScript zu verwenden ist eine Sache. TypeScript gut zu verwenden ist eine andere. Dieser Ratgeber sammelt die wichtigsten Muster und Anti-Patterns, die den Unterschied zwischen einem ärgerlichen und einem produktiven TypeScript-Erlebnis ausmachen.

Strict Mode aktivieren

Das Wichtigste zuerst: Aktiviere strict: true in deiner tsconfig.json. Dieses Flag schaltet eine Reihe von Checks ein, die zusammen ein zuverlässiges Typsystem ergeben.

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler"
  }
}

strict aktiviert unter anderem strictNullChecks, noImplicitAny und strictFunctionTypes. Wer diese Checks einzeln deaktiviert, um Fehler zu umgehen, verliert genau den Schutz, für den TypeScript da ist.

any vermeiden, unknown bevorzugen

any deaktiviert das Typsystem vollständig. unknown ist die typsichere Alternative, wenn du den Typ zur Laufzeit noch nicht kennst.

// Schlecht: any deaktiviert alle Prüfungen
function verarbeiteApiAntwort(daten: any) {
  return daten.nutzer.name; // Kein Fehler, auch wenn nutzer fehlt
}

// Gut: unknown erzwingt Typprüfung
function verarbeiteApiAntwortSicher(daten: unknown) {
  if (
    typeof daten === "object" &&
    daten !== null &&
    "nutzer" in daten &&
    typeof (daten as { nutzer: unknown }).nutzer === "object"
  ) {
    // Erst nach der Prüfung ist der Zugriff sicher
    const nutzer = (daten as { nutzer: { name: string } }).nutzer;
    return nutzer.name;
  }
  throw new Error("Unerwartetes Antwortformat");
}

Interface vs. Type

Beide definieren Typen, aber sie haben unterschiedliche Stärken:

// Interface: gut für erweiterbare Objekte und Klassen-Verträge
interface Tier {
  name: string;
  laut(): string;
}

interface Hund extends Tier {
  rasse: string;
}

// Type: gut für Union Types und Berechnungen
type Farbe = "rot" | "gruen" | "blau";
type Antwort = "ja" | "nein" | "vielleicht";

type Ergebnis<T> = { erfolg: true; daten: T } | { erfolg: false; fehler: string };

Wähle eine Konvention und halte dich daran. Im Team ist Konsistenz wichtiger als die richtige Wahl in Einzelfällen.

Enums mit Bedacht einsetzen

Klassische enum-Deklarationen haben Nachteile: Sie erzeugen JavaScript-Laufzeitcode und funktionieren nicht gut mit einigen Build-Tools.

// Schlecht: Klassisches Enum erzeugt Runtime-Code
enum Status {
  Aktiv = "aktiv",
  Inaktiv = "inaktiv",
  Gesperrt = "gesperrt",
}

// Besser: const Enum oder Union Type
const Status = {
  Aktiv: "aktiv",
  Inaktiv: "inaktiv",
  Gesperrt: "gesperrt",
} as const;

type Status = (typeof Status)[keyof typeof Status];
// Typ: "aktiv" | "inaktiv" | "gesperrt"

// Oder direkt als Union Type
type NutzerStatus = "aktiv" | "inaktiv" | "gesperrt";

Union Types aus String-Literals sind in den meisten Fällen die sauberere und leichtgewichtigere Lösung.

Readonly konsequent verwenden

Unveränderbare Daten zu markieren verhindert versehentliche Mutationen und macht den Datenfluss klarer.

interface Konfiguration {
  readonly apiUrl: string;
  readonly maxVersuche: number;
  readonly erlaubteHerkunftsdomaenen: readonly string[];
}

function initialisiereApp(config: Readonly<Konfiguration>) {
  // Kein Zugriff zum Ändern möglich
  console.log(`API: ${config.apiUrl}`);
}

Diskriminierte Unions statt optionaler Felder

Wenn ein Objekt in verschiedenen Zuständen existiert, sind diskriminierte Unions klarer als ein Haufen optionaler Felder.

// Schlecht: Alle Felder optional, unklare Zustände
interface Anfrage {
  status: "ausstehend" | "erfolgreich" | "fehlgeschlagen";
  daten?: string[];
  fehler?: string;
  ladezeit?: number;
}

// Gut: Jeder Zustand hat genau die richtigen Felder
type Anfrage =
  | { status: "ausstehend" }
  | { status: "erfolgreich"; daten: string[]; ladezeit: number }
  | { status: "fehlgeschlagen"; fehler: string };

function zeigeAnfrage(anfrage: Anfrage) {
  switch (anfrage.status) {
    case "ausstehend":
      return "Lädt...";
    case "erfolgreich":
      return `${anfrage.daten.length} Ergebnisse in ${anfrage.ladezeit}ms`;
    case "fehlgeschlagen":
      return `Fehler: ${anfrage.fehler}`;
  }
}

TypeScript prüft, ob alle Zustände behandelt werden. Vergisst du einen Fall, gibt es einen Compiler-Fehler.

Naming Conventions

Klare Konventionen machen Typdefinitionen lesbar:

// Interfaces: PascalCase, kein "I"-Prefix
interface NutzerProfil {}

// Types: PascalCase
type Suchparameter = string | number;

// Generische Typparameter: T, U, K, V oder sprechende Namen
type Repository<TEntitaet, TId> = {
  findeNachId(id: TId): TEntitaet | null;
  speichere(entitaet: TEntitaet): void;
};

// Enums und Konstanten: PascalCase
const HttpStatus = {
  OK: 200,
  NotFound: 404,
  ServerError: 500,
} as const;

Funktionen mit klaren Signaturen

Explizite Rückgabetypen bei exportierten Funktionen dokumentieren die API und fangen Fehler bei Refactorings früh ab.

// Gut: Expliziter Rückgabetyp bei öffentlicher Funktion
export function berechneGesamtpreis(
  artikel: Artikel[],
  mehrwertsteuersatz: number
): number {
  const netto = artikel.reduce((sum, a) => sum + a.preis, 0);
  return netto * (1 + mehrwertsteuersatz / 100);
}

Bei privaten Hilfsfunktionen ist der abgeleitete Typ meist ausreichend.

Im Playground mit Best Practices experimentieren

Das Playground bietet eine schnelle Umgebung, um verschiedene Muster auszuprobieren. Teste dort diskriminierte Unions, Readonly-Typen oder generische Funktionen, bevor du sie in ein Projekt einbaust. Die direkte Typrückmeldung im Editor zeigt sofort, ob ein Muster so funktioniert wie erwartet.

Fazit

Gutes TypeScript beginnt mit strict: true und endet nie wirklich. Je mehr du das Typsystem ausnutzt, desto mehr Fehler findet es für dich, bevor sie in Produktion landen. Vermeide any, bevorzuge unknown, nutze diskriminierte Unions für Zustände und markiere unveränderbare Daten mit Readonly. Das sind die Muster, die professionellen TypeScript-Code von hastigem TypeScript-Code unterscheiden.

Häufige Fragen

Soll ich interface oder type für meine Typendefinitionen verwenden?

Interfaces sind bevorzugt für Objekte, die öffentlich als API dienen oder durch andere Interfaces erweitert werden sollen. Types sind besser für Union Types, Intersection Types und komplexe Typberechnungen. Mische beide nicht durcheinander, wenn du eine davon konsequent als Standard gewählt hast.

Wann sollte ich 'any' verwenden?

Praktisch nie. 'any' schaltet das Typsystem aus und macht TypeScript wertlos. Als temporäre Lösung beim Migrieren von JavaScript-Code ist 'unknown' fast immer besser, weil es Typprüfungen erzwingt, bevor du den Wert verwendest.

Quellen

  • TypeScript Do's and Don'ts (typescriptlang.org/docs)
Jan-Tristan Rudat

Über die Autorenschaft

Jan-Tristan Rudat

Redakteur typescript-playground.de

Themengebiet: Generationen, Kulturgeschichte, Sternzeichen, Pop-Phänomene rund ums Alter

Mehr über Jan-Tristan Rudat →

Verwandte Artikel

TypeScript Playground nutzen

Sofort im Browser, ohne Anmeldung.

Zum Playground