unknown, any und never in TypeScript: Welcher wann?

1. Übersicht

TypeScript hat drei spezielle Typen, die leicht verwechselt werden: any, unknown und never. Alle drei erscheinen an den “Grenzen” des Typensystems. any deaktiviert die Typprüfung, unknown zwingt dich zur Überprüfung vor der Verwendung, und never stellt einen unmöglichen Fall dar.

Nach dem Lesen dieses Artikels wirst du wissen, welchen Typ du für Daten mit unbekannter Herkunft wählst. Du wirst auch wissen, wie du never verwendest, um den Compiler einen Fehler melden zu lassen, wenn du einen Zweig vergessen hast zu behandeln.

Alle Beispiele laufen mit TypeScript 5.x und "strict": true in tsconfig.json.

2. any: Typprüfung deaktivieren

any ermöglicht es einem Wert, alles zu tun. Der Compiler führt keine Überprüfung durch:

let payload: any = "Hallo";

payload.toUpperCase();   // OK
payload.foo.bar();       // OK zur Compile-Zeit, Fehler zur Laufzeit
payload = 42;
const total: number = payload * 2; // OK

Die dritte Zeile wird normal kompiliert. Zur Laufzeit wirft sie einen Fehler:

TypeError: Cannot read properties of undefined (reading 'bar')

Mit any geht TypeScript davon aus, dass du die Verantwortung selbst übernommen hast. Du verlierst Autocomplete, die Überprüfung von Eigenschaftsnamen und die Vorteile des sicheren Refacturings.

2.1. any breitet sich aus

Das größte Problem von any ist, dass es sich auf alles ausbreitet, das es berührt:

function loadConfig(): any {
  return JSON.parse('{"port": "8080"}');
}

const config = loadConfig();
const port = config.port;      // port: any
const next = port + 1;         // next: any
console.log(next);
80801

port ist der String "8080", also ergibt port + 1 "80801" statt 8081. Der Compiler gibt keine Warnung aus, da sich any bereits zu config, port und dann next verbreitet hat. Ein einzelnes any am Anfang kann die Typprüfung über eine ganze Kette von Funktionsaufrufen hinweg zum Schweigen bringen.

Beachte: JSON.parse, response.json() in der Fetch API und viele ältere Bibliotheken geben any zurück. Dies ist die häufigste Quelle für any-Lecks in echtem Code.

3. unknown: Sicher mit unbekanntem Typ

unknown akzeptiert auch jeden Wert wie any. Der Unterschied ist, dass du den Wert nicht verwenden darfst, bis du den Typ eingegrenzt hast (narrowing):

let payload: unknown = "Hallo";

payload.toUpperCase();
error TS18046: 'payload' is of type 'unknown'.

Der Compiler stoppt sofort. Du musst zuerst nachweisen, dass payload ein String ist:

if (typeof payload === "string") {
  console.log(payload.toUpperCase()); // payload: string
}
HALLO

Innerhalb des if-Blocks weiß TypeScript, dass payload ein string ist, daher ist toUpperCase() gültig.

3.1. Wege, unknown einzugrenzen

Es gibt vier Hauptwerkzeuge:

class PaymentError extends Error {
  constructor(message: string, public readonly code: string) {
    super(message);
  }
}

function describe(value: unknown): string {
  if (typeof value === "number") {
    return `Zahl: ${value.toFixed(2)}`;
  }
  if (value instanceof PaymentError) {
    return `Zahlungsfehler ${value.code}`;
  }
  if (typeof value === "object" && value !== null && "id" in value) {
    return `Objekt mit id: ${String(value.id)}`;
  }
  return "Unbekannt";
}

console.log(describe(12.5));
console.log(describe(new PaymentError("Geld aufgebraucht", "INSUFFICIENT")));
console.log(describe({ id: 7 }));
console.log(describe(null));
Zahl: 12.50
Zahlungsfehler INSUFFICIENT
Objekt mit id: 7
Unbekannt
  • typeof wird für primitive Typen verwendet: string, number, boolean…
  • instanceof wird für Klassen verwendet.
  • in prüft auf die Existenz einer Eigenschaft. Ab TypeScript 4.9 hat value den Typ object & Record<"id", unknown> nach "id" in value.
  • Du musst value !== null prüfen, da typeof null === "object".

3.2. Type Guards für externe Daten

Bei komplexen Objekten solltest du einen Type Guard schreiben, das ist eine Funktion, die value is T zurückgibt:

interface Order {
  id: string;
  customerName: string;
  total: number;
}

export function isOrder(value: unknown): value is Order {
  if (typeof value !== "object" || value === null) return false;
  const v = value as Record<string, unknown>;
  return (
    typeof v.id === "string" &&
    typeof v.customerName === "string" &&
    typeof v.total === "number"
  );
}

export function parseOrder(json: string): Order {
  const data: unknown = JSON.parse(json);
  if (!isOrder(data)) {
    throw new Error("Bestelldaten sind ungültig");
  }
  return data; // data: Order
}

console.log(parseOrder('{"id":"A1","customerName":"Anna","total":250000}'));
{ id: 'A1', customerName: 'Anna', total: 250000 }

Der Schlüsselpunkt ist die Zeile const data: unknown = JSON.parse(json). Die explizite Typzuweisung unknown stoppt, dass sich any von JSON.parse ausbreitet. Von da an zwingt dich der Compiler, data zu überprüfen, bevor du es verwendest.

In größeren Projekten kannst du den handgeschriebenen Type Guard durch eine Schema-Validierungsbibliothek wie Zod ersetzen. Das Prinzip bleibt gleich: Eingabedaten sind unknown, und nur nach der Validierung werden sie zu einem bestimmten Typ.

3.3. unknown im catch

Mit strict: true (speziell das Flag useUnknownInCatchVariables) hat die Variable in catch den Typ unknown:

try {
  parseOrder("{}");
} catch (err) {
  // err: unknown
  const message = err instanceof Error ? err.message : String(err);
  console.log(message);
}
Bestelldaten sind ungültig

Dies entspricht der Realität. JavaScript erlaubt dir, alles zu throw, auch Strings oder Zahlen, daher kannst du nicht davon ausgehen, dass err immer ein Error ist.

4. never: Typ ohne Werte

never ist ein leerer Typ. Es gibt keinen Wert, der zu never gehört. Es erscheint in drei Hauptsituationen.

4.1. Funktionen, die nie zurückkehren

Eine Funktion, die immer einen Fehler wirft oder eine Endlosschleife läuft, hat den Rückgabetyp never:

function fail(message: string): never {
  throw new Error(message);
}

function getDiscount(code: string): number {
  if (code === "TET2026") return 0.2;
  return fail(`Rabattcode existiert nicht: ${code}`);
}

Da sich never jedem Typ zuweisen lässt, ist return fail(...) in einer Funktion, die number zurückgibt, gültig. Der Compiler versteht auch, dass Code nach dem Aufruf von fail() unerreichbar ist.

4.2. Exhaustive Checks

Dies ist die nützlichste Anwendung von never. Angenommen, du hast eine Union von Zahlungsmethoden:

type PaymentMethod =
  | { kind: "card"; last4: string }
  | { kind: "cash" }
  | { kind: "momo"; phone: string };

export function assertNever(value: never): never {
  throw new Error(`Nicht behandelter Fall: ${JSON.stringify(value)}`);
}

export function paymentLabel(method: PaymentMethod): string {
  switch (method.kind) {
    case "card":
      return `Karte **** ${method.last4}`;
    case "cash":
      return "Bargeld";
    case "momo":
      return `MoMo ${method.phone}`;
    default:
      return assertNever(method);
  }
}

Nach der Behandlung aller drei case-Zweige wird method im default-Zweig auf never eingegrenzt. Der Aufruf von assertNever(method) ist daher gültig.

Jetzt stell dir vor, ein Kollege fügt { kind: "zalopay"; phone: string } zur Union hinzu, aber vergisst, das switch zu aktualisieren. Der Compiler meldet sofort:

error TS2345: Argument of type '{ kind: "zalopay"; phone: string; }' is not assignable to parameter of type 'never'.

Der Fehler wird zur Compile-Zeit erkannt, anstatt dass Benutzer eine undefined-Bezeichnung auf dem Zahlungsbildschirm sehen. Falls seltsame Daten zur Laufzeit durchkommen, z.B. von einer alten API, wird assertNever einen klaren Fehler werfen.

4.3. never in Conditional Types

In Conditional Types wird never verwendet, um ein Mitglied aus einer Union zu entfernen, da A | never genau A ist:

type OnlyStrings<T> = T extends string ? T : never;

type Mixed = "vnd" | "usd" | 404 | null;
type Currencies = OnlyStrings<Mixed>; // "vnd" | "usd"

TypeScript verteilt den Conditional Type auf jedes Mitglied der Union. Die Mitglieder 404 und null werden zu never und verschwinden dann. Die eingebauten Utility Types wie Exclude, Extract und NonNullable funktionieren alle nach diesem Mechanismus.

5. Schneller Vergleich

Der Kernunterschied liegt in der Zuweisungskompatibilität (assignability):

Andere Werte in diesen Typ zuweisen Diesen Typ anderen Typen zuweisen Direkt verwenden
any Ja Ja (außer never) Ja, keine Überprüfung
unknown Ja Nur auf unknown, any Nein, nur mit Narrowing
never Nein (außer never) Ja, zu jedem Typ Keine Werte vorhanden

Man kann es wie folgt zusammenfassen:

  • unknown ist der sichere Top-Typ. Alles ist unknown.
  • never ist der Bottom-Typ. never ist ein Subtyp aller anderen Typen.
  • any ist ein Schlupfloch. Es spielt sowohl die Rolle des Top-Typs als auch des Bottom-Typs und bricht daher die Solidität des Typensystems.

6. Testen von Verhalten

Du kannst sowohl das Laufzeitverhalten als auch die Compile-Zeit-Typen mit Vitest testen. Installation:

npm install -D vitest typescript

Angenommen, die Funktionen aus den Abschnitten 3.2 und 4.2 sind in order.ts und payment.ts:

// payment.test.ts
import { describe, it, expect, expectTypeOf } from "vitest";
import { isOrder, parseOrder } from "./order";
import { paymentLabel, assertNever } from "./payment";

describe("unknown und Type Guard", () => {
  it("akzeptiert gültige Bestellungen", () => {
    const order = parseOrder('{"id":"A1","customerName":"Anna","total":1000}');
    expect(order.total).toBe(1000);
    expectTypeOf(order.total).toEqualTypeOf<number>();
  });

  it("lehnt Daten mit falschem Typ ab", () => {
    expect(isOrder({ id: "A1", customerName: "Anna", total: "1000" })).toBe(false);
    expect(() => parseOrder("null")).toThrow("Bestelldaten sind ungültig");
  });
});

describe("never und Exhaustive Check", () => {
  it("gibt die richtige Bezeichnung für jede Methode zurück", () => {
    expect(paymentLabel({ kind: "card", last4: "4242" })).toBe("Karte **** 4242");
    expect(paymentLabel({ kind: "cash" })).toBe("Bargeld");
  });

  it("wirft einen Fehler, wenn es zur Laufzeit Daten außerhalb der Union erhält", () => {
    const legacy = { kind: "paypal" } as unknown as never;
    expect(() => assertNever(legacy)).toThrow("Nicht behandelter Fall");
  });
});
npx vitest run
 ✓ payment.test.ts (4 tests)
 Test Files  1 passed (1)
      Tests  4 passed (4)

expectTypeOf überprüft den Typ zur Compile-Zeit. Wenn eine Änderung order.total zu any macht, wird der Test bei der Ausführung des Type Check fehlschlagen (vitest --typecheck). Der letzte Test simuliert “seltsame” Daten von einem Legacy-System, um sicherzustellen, dass das Laufzeit-Sicherheitsnetz immer noch funktioniert.

7. Häufige Fehler und wann man sie nicht verwenden sollte

any verwenden, um Compile-Fehler zu “übergehen”. Das ist die schlechteste Gewohnheit. Wenn du den Typ wirklich nicht kennst, verwende unknown. Aktiviere die Regel @typescript-eslint/no-explicit-any, um diese Fälle zu erfassen.

unknown mit as casten statt zu Narrowing. data as Order beruhigt nur den Compiler, führt aber zur Laufzeit keine Überprüfung durch. Im Grunde ist es genauso gefährlich wie any.

const data: unknown = JSON.parse('{"id": 1}');
const order = data as Order; // compile OK
order.customerName.trim();   // TypeError zur Laufzeit

unknown überall verwenden. unknown ist für die Systemgrenzen: Benutzereingaben, API-Antworten, catch, deserialisierte Daten. Innerhalb einer Domain verwende spezifische Typen. Eine Funktion wie calculateTotal(items: unknown) ist ein schlechtes Design.

never mit void verwechseln. void bedeutet, die Funktion kehrt zurück, hat aber keinen sinnvollen Wert. never bedeutet, die Funktion kehrt nie zurück. Eine normale log()-Funktion sollte void sein, nicht never.

default im switch vergessen. Ohne assertNever meldet TypeScript einen Fehler nur, wenn die Funktion einen expliziten Rückgabetyp hat und noImplicitReturns aktiviert ist. Selbst dann verlierst du das Laufzeit-Sicherheitsnetz.

Beachte: any hat immer noch legitime Anwendungsfälle. Das ist bei der schrittweisen Migration eines großen JavaScript-Codebases oder beim Schreiben von Deklarationen für Bibliotheken mit sehr dynamischen Typen der Fall. Begrenze es eng und hinterlasse einen Kommentar, der den Grund erklärt.

8. Fazit

Diese drei Typen haben drei klare Rollen:

  • Verwende unknown für Daten mit unbekanntem Typ an Systemgrenzen, wende dann Narrowing mit typeof, instanceof, in oder Type Guards an.
  • Verwende never für Funktionen, die immer Fehler werfen, für Exhaustive Checks mit assertNever, und um Unions in Conditional Types zu filtern.
  • Vermeide any, es sei denn, du hast einen bestimmten Grund und hast es eng begrenzt.

Die praktische Regel ist sehr einfach. Wenn du any tippen willst, versuche zuerst unknown. Wenn der Compiler einen Fehler meldet, sind das genau die Stellen, an denen du Daten überprüfen musst.

typescripttype-safetyunknownneverany