TypeScript-Fehler, die jeder Entwickler macht (und wie man sie behebt)

2. Juni 2026

TypeScript-Fehler, die jeder Entwickler macht (und wie man sie behebt)

TypeScript hat sich rasant zum Industriestandard für die Frontend- und Full-Stack-Entwicklung entwickelt. Es verspricht Typsicherheit, zuverlässiges Refactoring und selbstdokumentierenden Code in der ansonsten dynamischen und fehleranfälligen Welt von JavaScript. Richtig eingesetzt agiert TypeScript wie ein aufmerksamer Pair-Programming-Partner, der Fehler findet, bevor sie überhaupt in die Produktion gelangen.

Viele Entwickler behandeln TypeScript jedoch eher als ein Hindernis, das es zu umgehen gilt, statt als ein System, mit dem man kooperieren sollte. Durch die Verwendung von Typ-Schlupflöchern, den Missbrauch von Typ-Zuweisungen oder die Überkonstruktion generischer Signaturen enden Teams oft mit einer Codebasis, die zwar fehlerfrei kompiliert, aber zur Laufzeit dennoch abstürzt. In diesem detaillierten Artikel untersuchen wir die häufigsten architektonischen und syntaktischen TypeScript-Fehler, analysieren ihre Ursachen und implementieren professionelle, typsichere Lösungen.


1. Der übermäßige Gebrauch von any und die sicherere Alternative unknown

Der Typ any ist der ultimative Deaktivierungsknopf für den Compiler. Wenn Sie eine Variable als any deklarieren, weisen Sie den Compiler an, jegliche Typprüfung für diese Variable und alle darauf folgenden Ausdrücke zu ignorieren. Das bedeutet im Grunde eine Rückkehr zu reinem JavaScript.

// Unsichere Praxis: Deaktiviert alle Schutzmechanismen des Compilers
function processUserData(user: any) {
  // Wenn user oder user.settings undefined ist, kommt es zur Laufzeit zum Absturz
  const theme = user.settings.theme.toLowerCase();
  console.log(`Setze Theme auf: ${theme}`);
}

Wenn Sie den Typ eingehender Daten nicht kennen (z. B. bei Daten von einer externen API oder beim Parsen von Dateien), führt die Verwendung von any zu unmittelbaren Sicherheitslücken. Verwenden Sie stattdessen den Typ unknown. Der Typ unknown erlaubt zwar jeden Wert, aber der Compiler zwingt Sie dazu, eine Typ-Eingrenzung (Type Narrowing) oder eine Validierung durchzuführen, bevor Sie Operationen auf der Variablen ausführen können.

Typ-Eingrenzung mit benutzerdefinierten Type Guards

Um sicher mit unknown zu arbeiten, können Sie einen benutzerdefinierten Type Guard mit dem Schlüsselwort is erstellen:

interface UserProfile {
  id: string;
  settings: {
    theme: string;
  };
}

// Benutzerdefinierter Type Guard
function isUserProfile(value: unknown): value is UserProfile {
  if (typeof value !== 'object' || value === null) {
    return false;
  }
  
  const candidate = value as Record<string, unknown>;
  
  if (typeof candidate.id !== 'string') {
    return false;
  }
  
  if (typeof candidate.settings !== 'object' || candidate.settings === null) {
    return false;
  }
  
  const settingsCandidate = candidate.settings as Record<string, unknown>;
  return typeof settingsCandidate.theme === 'string';
}

function processSafeUserData(rawInput: unknown) {
  if (isUserProfile(rawInput)) {
    // Innerhalb dieses Blocks weiß der Compiler sicher, dass rawInput ein UserProfile ist
    const theme = rawInput.settings.theme.toLowerCase();
    console.log(`Setze Theme auf: ${theme}`);
  } else {
    console.error("Ungültiges Format für das Benutzerprofil erhalten");
  }
}

Durch den Wechsel von any zu unknown verlagern Sie die Verantwortung für die Typsicherheit auf die Laufzeitlogik und stellen sicher, dass Ihre Anwendung unerwartete payloads sauber abfängt.


2. Der Missbrauch von Typ-Zusicherungen (as) und wie Zod-Validierung zur Laufzeit hilft

Typ-Zusicherungen (as) teilen dem Compiler mit: "Ich weiß es besser als du, vertrau mir einfach." Während sie beim Umgang mit DOM-Methoden oder Altsystemen gelegentlich notwendig sind, missbrauchen Entwickler sie häufig, um Kompilierungsfehler schnell zum Schweigen zu bringen.

interface DBUser {
  id: string;
  email: string;
  role: 'admin' | 'editor' | 'viewer';
}

// Hochgradig unsicher: Nimmt an, dass die API-Antwort exakt dem DBUser-Typ entspricht
const fetchUserFromAPI = async (id: string): Promise<DBUser> => {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return data as DBUser; 
};

Wenn sich das API-Schema ändert oder leere Daten zurückgegeben werden, entspricht das erhaltene Objekt nicht der Struktur von DBUser. Dies führt zu Fehlern wie Cannot read properties of undefined an einer späteren Stelle in Ihrer Anwendung.

Zusicherungen durch Schema-Validierung zur Laufzeit (Zod) ersetzen

Statt unsichere Typ-Zusicherungen zu verwenden, sollten Sie eingehende Daten mit einer Schema-Validierungsbibliothek wie Zod validieren. Dies stellt sicher, dass Ihre Typen zur Build-Zeit perfekt mit den tatsächlichen Laufzeitdaten übereinstimmen.

import { z } from 'zod';

// Definieren des Laufzeitschemas
const DBUserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  role: z.union([
    z.literal('admin'),
    z.literal('editor'),
    z.literal('viewer')
  ])
});

// Ableiten des TypeScript-Typs direkt aus dem Schema
type DBUser = z.infer<typeof DBUserSchema>;

const fetchSafeUserFromAPI = async (id: string): Promise<DBUser> => {
  const response = await fetch(`/api/users/${id}`);
  const rawData = await response.json();
  
  // Überprüft die Datenstruktur zur Laufzeit und wirft bei Fehlern eine Exception
  const validatedUser = DBUserSchema.parse(rawData);
  return validatedUser;
};

Durch den Übergang von Typ-Zusicherungen zu Schema-Validierungen errichten Sie eine klare Grenze an den Rändern Ihrer Anwendung und weisen fehlerhafte Daten sofort ab.


3. Die Überkonstruktion von Generics vs einfache Schnittstellen-Polymorphie

Generics sind eine der mächtigsten Funktionen in TypeScript und ermöglichen die Wiederverwendbarkeit von Code. Ein häufiger Fehler ist jedoch die übermäßige Verwendung von Generics an Stellen, an denen eine einfache Schnittstelle oder ein Union-Typ viel verständlicher und lesbarer wäre.

Betrachten Sie diese überkonstruierte generische Funktion:

// Unnötig komplexe generische Signatur
function getRecordId<T extends { id: string }>(record: T): string {
  return record.id;
}

Die Verwendung eines Generics bringt hier keinen Nutzen, da die Funktion den Typ des Arguments nicht dynamisch auf den Rückgabetyp abbilden muss. Der Rückgabetyp ist immer ein einfacher String. Dies lässt sich durch Schnittstellen-Polymorphie deutlich vereinfachen:

interface Identifiable {
  id: string;
}

// Einfache und lesbare Alternative
function getRecordIdSimple(record: Identifiable): string {
  return record.id;
}

Wann Sie Generics tatsächlich verwenden sollten

Generics sollten nur dann verwendet werden, wenn eine direkte Beziehung zwischen den Typen verschiedener Parameter oder zwischen den Parametern und dem Rückgabetyp der Funktion besteht. Zum Beispiel:

// Korrekte Verwendung: Ordnet die Elemente des Eingabearrays dem Typ des Transformationsergebnisses zu
function mapRecord<T, U>(items: T[], transformer: (item: T) => U): U[] {
  return items.map(transformer);
}

Fragen Sie sich vor dem Hinzufügen von <T> immer, ob Sie den Typenparameter benötigen, um zwei oder mehr Elemente miteinander in Beziehung zu setzen. Wenn nicht, verwenden Sie Standard-Schnittstellen.


4. Missverständnisse bei struktureller Typisierung und überschüssigen Eigenschaften

JavaScript ist von Natur aus dynamisch. TypeScript bildet dieses Verhalten durch ein strukturelles Typsystem ab (oft als Duck Typing bezeichnet). Wenn zwei Objekte dieselbe Form haben, gelten sie als typkompatibel, unabhängig davon, wie sie instanziiert wurden.

interface Point2D {
  x: number;
  y: number;
}

function printCoordinates(point: Point2D) {
  console.log(`x: ${point.x}, y: ${point.y}`);
}

const point3D = { x: 10, y: 20, z: 30 };
printCoordinates(point3D); // Kompiliert ohne Probleme!

Da point3D sowohl über x als auch über y verfügt, erfüllt es die strukturellen Bedingungen von Point2D. TypeScript verhält sich jedoch anders, wenn Sie Objektliterale direkt an eine Funktion übergeben (Prüfung auf überschüssige Eigenschaften - excess property checking):

// Fehler: Objektliteral darf nur bekannte Eigenschaften angeben, 'z' existiert in 'Point2D' nicht
printCoordinates({ x: 10, y: 20, z: 30 });

Diese strenge Prüfung soll Tippfehler verhindern. Sie verwirrt jedoch Entwickler, die nicht verstehen, warum das Zuweisen des Objekts an eine Variable vor der Übergabe den Fehler behebt.

Index-Signaturen sicher verwenden

Wenn Ihr Objekt dynamische zusätzliche Eigenschaften unterstützen muss, definieren Sie eine Index-Signatur:

interface DynamicPoint {
  x: number;
  y: number;
  [key: string]: unknown; // Erlaubt explizit weitere Eigenschaften
}

function printDynamicCoordinates(point: DynamicPoint) {
  console.log(`x: ${point.x}, y: ${point.y}`);
}

// Kompiliert nun fehlerfrei, auch mit direktem Objektliteral
printDynamicCoordinates({ x: 10, y: 20, z: 30 });

5. Optionale Eigenschaften überall vs Discriminated Unions

Beim Entwerfen von Anwendungszuständen erstellen Entwickler häufig eine einzige Schnittstelle, bei der jede Eigenschaft optional ist, um verschiedene Phasen eines asynchronen Prozesses abzubilden.

// Unsichere Zustandsdarstellung
interface FetchState {
  data?: string;
  error?: string;
  isLoading: boolean;
}

Dieses Design ermöglicht unmögliche Zustände, wie z. B. das gleichzeitige Vorhandensein von data und error oder ein Zustand mit isLoading: false, bei dem sowohl data als auch error fehlen. Code, der diesen Zustand konsumiert, erfordert viele verschachtelte Prüfungen.

// Die Verarbeitung dieses Zustands wird fehleranfällig
function renderState(state: FetchState) {
  if (state.isLoading) {
    return "Lade...";
  }
  if (state.error) {
    return `Fehler: ${state.error}`;
  }
  if (state.data) {
    return `Daten: ${state.data}`;
  }
  return "Unbekannter Zustand"; // Code-Geruch
}

Zustandsautomaten mit Discriminated Unions entwerfen

Ein wesentlich besserer Ansatz ist die Verwendung einer Discriminated Union. Durch die Einführung einer gemeinsamen literalen Eigenschaft (dem Diskriminanten) teilen wir den Zustand in sich gegenseitig ausschließende Typen auf:

type FetchState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: string }
  | { status: 'error'; error: Error };

function renderSafeState(state: FetchState) {
  switch (state.status) {
    case 'idle':
      return "Bereit zum Laden";
    case 'loading':
      return "Lade...";
    case 'success':
      // TypeScript grenzt den Typ hier automatisch auf den Success-Zustand ein
      return `Daten: ${state.data}`;
    case 'error':
      // TypeScript grenzt den Typ hier automatisch auf den Error-Zustand ein
      return `Fehler: ${state.error.message}`;
  }
}

Diese Architektur stellt sicher, dass der Zustand niemals in einer ungültigen Kombination instanziiert werden kann, was Boilerplate-Prüfungen überflüssig macht.


6. Die Schwächen des integrierten Typs Omit und die Erstellung von StrictOmit

TypeScript bietet praktische Hilfstypen wie Pick, Omit und Exclude. Der integrierte Typ Omit hat jedoch eine Schwachstelle: Er prüft nicht, ob der zu entfernende Schlüssel tatsächlich auf dem Zielobjekt existiert.

interface UserConfig {
  theme: string;
  notifications: boolean;
  version: number;
}

// Kein Kompilierungsfehler, obwohl 'nonExistentKey' keine Eigenschaft von UserConfig ist
type TruncatedConfig = Omit<UserConfig, 'version' | 'nonExistentKey'>;

Dieses Verhalten macht den Code anfällig für unbemerkt bleibende Fehler beim Refactoring. Wenn Sie eine Eigenschaft in UserConfig umbenennen, funktioniert der Omit-Aufruf weiterhin geräuschlos, wodurch veraltete Schlüssel im Code verbleiben.

Definition und Verwendung von StrictOmit

Wir können unseren eigenen Hilfstyp definieren, der erzwingt, dass die zu entfernenden Schlüssel eine Teilmenge der Schlüssel des Typs sein müssen:

// Definieren von StrictOmit mit Schlüsselprüfung via keyof
type StrictOmit<T, K extends keyof T> = Omit<T, K>;

// Dies führt nun zu einem Kompilierungsfehler bei ungültigen Schlüsseln
type SafeConfig = StrictOmit<UserConfig, 'version' | 'nonExistentKey'>;
                   Vergleich von Omit und StrictOmit
                   
+------------------------------------+------------------------------------+
  Omit<UserConfig, 'invalidKey'>     | StrictOmit<UserConfig, 'invalidKey'>
+------------------------------------+------------------------------------+
  Kompiliert ohne Fehler             | Führt zu einem Kompilierungsfehler 
  Ignoriert nicht vorhandene Schlüssel| Erfordert Pflege der Schlüssel     
  Risiko veralteter Mappings         | Garantiert Typintegrität           
+------------------------------------+------------------------------------+

Durch die Verwendung von StrictOmit in Ihrem Projekt stellen Sie sicher, dass zukünftige Anpassungen der Typstruktur keine toten Verweise hinterlassen.


7. Nebenwirkungen der Veränderlichkeit: Fehlende readonly-Modifikatoren

In einer sauberen Softwarearchitektur sollten Funktionen ihre Argumente nicht modifizieren, um unvorhersehbare Nebenwirkungen (Side Effects) an anderen Stellen der Anwendung zu vermeiden. Standardmäßig sind Objekte und Arrays in TypeScript veränderlich (mutable).

interface Cart {
  items: string[];
}

function clearCartItem(cart: Cart, index: number) {
  // Mutiert das ursprüngliche Objekt direkt
  cart.items.splice(index, 1); 
}

Um ungewollte Datenänderungen zu verhindern, deklarieren Sie Ihre Schnittstellen und Arrays als schreibgeschützt.

Verwendung von readonly-Eigenschaften und ReadonlyArray

Durch das Voranstellen von readonly vor Eigenschaften und die Verwendung von ReadonlyArray<T> (oder der Kurzschreibweise readonly T[]) verhindert der Compiler den Aufruf mutierender Methoden:

interface ImmutableCart {
  readonly items: readonly string[];
}

function safeClearCartItem(cart: ImmutableCart, index: number): ImmutableCart {
  // cart.items.splice(index, 1); // Fehler: Methode 'splice' existiert nicht auf 'readonly string[]'
  
  // Gibt eine neue Kopie zurück, anstatt das Original zu verändern
  return {
    items: cart.items.filter((_, idx) => idx !== index)
  };
}

Dieser Ansatz motiviert Entwickler dazu, reine Funktionen (Pure Functions) zu schreiben, was hervorragend zu modernen deklarativen und funktionalen Paradigmen passt.

Durch das Vermeiden dieser sieben typischen Fehler verwandeln Sie TypeScript von einem reinen Regelwerk, das man zu umgehen versucht, in ein echtes Sicherheitsnetz für die Qualität Ihres Codes.