Developer Tools

TypeScript Best Practices: Sichereren und wartbareren Code schreiben

Lerne die wichtigsten TypeScript-Muster – Strict Mode, Utility Types, Generics, Discriminated Unions und die Vermeidung gängiger Fluchtwege aus dem Typsystem.

8 Min. Lesezeit

Code-Editor mit TypeScript auf einem Monitor

TypeScript hat sich in den meisten professionellen JavaScript-Projekten vom optionalen Zusatz zur unverzichtbaren Grundlage entwickelt. Doch zwischen „technisch gesehen TypeScript" und „TypeScript, das tatsächlich Bugs abfängt" liegt ein großer Unterschied. Dieser Unterschied ergibt sich aus einer Handvoll Mustern, die die wahre Stärke des Typsystems entfesseln.

Strict Mode aktivieren – immer

Die einzelne TypeScript-Konfigurationsänderung mit dem größten Nutzen:

// tsconfig.json
{
  "compilerOptions": {
    "strict": true
  }
}

strict aktiviert ein Bündel von Prüfungen:

  • strictNullChecksnull und undefined sind anderen Typen nicht zuweisbar
  • noImplicitAny — Variablen können nicht implizit den Typ any haben
  • strictFunctionTypes — strengere Prüfung von Funktionsparametern
  • strictPropertyInitialization — Klasseneigenschaften müssen initialisiert werden

Ohne strictNullChecks übersieht TypeScript die häufigste Klasse von Laufzeitfehlern. Aktiviere es von Anfang an – es nachträglich in eine große Codebasis einzuführen ist mühsam.

any vermeiden – stattdessen unknown verwenden

any ist ein Fluchtweg aus dem Typsystem, der alle Fehler zum Schweigen bringt und den Sinn von TypeScript untergräbt:

// ❌ any deaktiviert alle Typprüfungen
function processData(data: any) {
  data.nonExistentMethod(); // Kein Fehler! Stürzt zur Laufzeit ab.
}

// ✅ unknown zwingt dich, den Typ einzugrenzen, bevor du ihn verwendest
function processData(data: unknown) {
  if (typeof data === "string") {
    console.log(data.toUpperCase()); // Sicher — auf string eingeengt
  }
  if (data instanceof Error) {
    console.log(data.message); // Sicher — auf Error eingeengt
  }
}

unknown ist die typsichere Version von any. Es zwingt dich, zu prüfen, womit du es zu tun hast, bevor du es verwendest. Nutze es für API-Antworten, JSON-Parsing und alles, was aus externen Quellen stammt.

Discriminated Unions für erschöpfendes Handling

Eines der mächtigsten Muster in TypeScript:

type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: User[] };
type ErrorState   = { status: "error"; message: string };

type ApiState = LoadingState | SuccessState | ErrorState;

function render(state: ApiState) {
  switch (state.status) {
    case "loading":
      return <Spinner />;
    case "success":
      return <UserList users={state.data} />; // state.data hier verfügbar
    case "error":
      return <ErrorMessage msg={state.message} />; // state.message hier verfügbar
    default:
      // TypeScript gibt hier einen Fehler aus, wenn ein neuer State ohne Behandlung hinzugefügt wird
      const _exhaustive: never = state;
      throw new Error(`Unhandled state: ${_exhaustive}`);
  }
}

Die never-Zuweisung am Ende ist der Exhaustiveness Check – wenn du eine neue Variante zu ApiState hinzufügst, ohne sie im Switch zu behandeln, gibt TypeScript einen Fehler zur Compile-Zeit aus. Dies ist eines der wertvollsten Sicherheitsnetze der gesamten Sprache.

Utility Types

TypeScript wird mit eingebauten Typ-Transformatoren ausgeliefert:

interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "editor" | "viewer";
  createdAt: Date;
}

// Alle Eigenschaften optional machen
type PartialUser = Partial<User>;

// Alle Eigenschaften erforderlich machen
type RequiredUser = Required<User>;

// Alle Eigenschaften schreibgeschützt machen
type ReadonlyUser = Readonly<User>;

// Bestimmte Eigenschaften auswählen
type UserPreview = Pick<User, "id" | "name">;

// Bestimmte Eigenschaften ausschließen
type UserWithoutDates = Omit<User, "createdAt">;

// Einen Typ aus den Werten eines Objekts erstellen
type UserRole = User["role"]; // "admin" | "editor" | "viewer"

// Eigenschaften nullable machen
type NullableUser = { [K in keyof User]: User[K] | null };

// Record-Typ: Objekt mit bestimmten Schlüssel- und Werttypen
type RolePermissions = Record<UserRole, string[]>;

// Aus Union Types extrahieren/ausschließen
type AdminOrEditor = Extract<UserRole, "admin" | "editor">; // "admin" | "editor"
type NonAdmin = Exclude<UserRole, "admin">; // "editor" | "viewer"

Generics für wiederverwendbaren, typsicheren Code

Generics ermöglichen es dir, Funktionen und Klassen zu schreiben, die mit beliebigen Typen arbeiten und dabei vollständig typsicher bleiben:

// Ohne Generics — verliert Typinformationen
function first(arr: any[]): any {
  return arr[0];
}

// Mit Generics — erhält den Typ
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const num = first([1, 2, 3]);     // Typ: number | undefined
const str = first(["a", "b"]);   // Typ: string | undefined

Generics einschränken

// T muss eine 'id'-Eigenschaft haben
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

// K muss ein Schlüssel von T sein
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Alice", role: "admin" };
const name = getProperty(user, "name");  // Typ: string
const role = getProperty(user, "role");  // Typ: string
// getProperty(user, "missing"); // ❌ TypeScript-Fehler — "missing" ist kein Schlüssel

Type Guards und Narrowing

// Type Predicate — benutzerdefinierter Type Guard
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value &&
    "email" in value
  );
}

// Verwendung
const data: unknown = await fetchUser();
if (isUser(data)) {
  console.log(data.name); // TypeScript weiß hier, dass data vom Typ User ist
}

Für die Validierung von API-Antworten zur Laufzeit kombiniere TypeScript-Interfaces mit Laufzeit-Schema-Validierung mithilfe von Zod. Generiere Zod-Schemas direkt aus deinem JSON mit unserem JSON to Zod-Konverter – füge eine JSON-Beispielantwort ein und erhalte sofort ein typsicheres Schema.

Template Literal Types

TypeScript 4.1+ unterstützt String-Manipulation auf Typebene:

type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus" | "onBlur"

type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSKey = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"

satisfies-Operator (TypeScript 4.9+)

Prüft, ob ein Wert einem Typ entspricht, ohne ihn zu erweitern:

type ColorMap = Record<string, string>;

// ❌ Ohne satisfies — Typ wird auf Record<string, string> erweitert
const colors: ColorMap = {
  primary: "#3b82f6",
  secondary: "#8b5cf6",
};
colors.primary.toUpperCase(); // Funktioniert, verliert aber den string-Typ

// ✅ Mit satisfies — validiert UND erhält Literal-Typen
const colors = {
  primary: "#3b82f6",
  secondary: "#8b5cf6",
} satisfies ColorMap;

colors.primary.toUpperCase(); // ✅ TypeScript weiß, dass es ein string ist
colors.nonexistent; // ❌ Fehler — Schlüssel existiert nicht

Typen aus Daten generieren

Anstatt TypeScript-Interfaces manuell aus API-Antworten zu schreiben:

  1. Füge eine JSON-Beispielantwort in unseren JSON to TypeScript-Konverter ein, um sofort Interfaces zu erhalten
  2. Oder generiere ein Zod-Schema mit JSON to Zod – Zod leitet TypeScript-Typen automatisch über z.infer<typeof schema> ab
import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  role: z.enum(["admin", "editor", "viewer"]),
});

// Typ wird automatisch abgeleitet — keine Duplizierung
type User = z.infer<typeof UserSchema>;

// Validiert zur Laufzeit UND stellt TypeScript-Typen bereit
const user = UserSchema.parse(await response.json());

Häufige Anti-Patterns vermeiden

Anti-Pattern Problem Lösung
as any Deaktiviert die Typprüfung unknown + Narrowing verwenden
// @ts-ignore Unterdrückt Fehler ohne Behebung Den Typfehler beheben
as SomeType (unsicherer Cast) Umgeht die Typinferenz Type Guards verwenden
interface für Unions Funktioniert nicht für Discriminated Unions type verwenden
Zu allgemeines object Keine Struktur Ein passendes Interface definieren
Function als Typ Keine Parameter-/Rückgabeinformationen Spezifische Signatur verwenden

Wissenswerte TypeScript-Konfiguration

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,  // array[n] gibt T | undefined zurück
    "exactOptionalPropertyTypes": true, // unterscheidet fehlend von undefined
    "noImplicitReturns": true,          // alle Codepfade müssen einen Wert zurückgeben
    "noFallthroughCasesInSwitch": true, // kein unbeabsichtigtes Switch-Fallthrough
    "verbatimModuleSyntax": true        // explizites import type für reine Typ-Imports
  }
}

Der Wert von TypeScript ist proportional dazu, wie konsequent du es einsetzt. Die hier vorgestellten Muster – Strict Mode, Discriminated Unions, Generics, Exhaustiveness Checks – sind das, was TypeScript, das Bugs findet, von TypeScript unterscheidet, das lediglich Syntax hinzufügt.