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.
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)
Ü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
Grundlagen
Was ist TypeScript? Eine Einführung in die typisierte JavaScript-Erweiterung
TypeScript erweitert JavaScript um statische Typen und macht große Codebases wartbarer. Lerne die Grundlagen, die Geschichte und die wichtigsten Vorteile von TypeScript.
Lesezeit 7 Min.
Grundlagen
TypeScript vs. JavaScript: Die wichtigsten Unterschiede im Überblick
TypeScript und JavaScript unterscheiden sich grundlegend in der Typprüfung, Werkzeugunterstützung und Wartbarkeit. Dieser Ratgeber erklärt die Unterschiede und hilft bei der Wahl der richtigen Sprache.
Lesezeit 8 Min.
Typen
TypeScript Typen Grundlagen: string, number, boolean und mehr
Die primitiven Typen string, number und boolean sind das Fundament von TypeScript. Dieser Ratgeber erklärt alle grundlegenden Typen mit praktischen Beispielen.
Lesezeit 9 Min.