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 gebenanyzurück. Dies ist die häufigste Quelle fürany-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
typeofwird für primitive Typen verwendet:string,number,boolean…instanceofwird für Klassen verwendet.inprüft auf die Existenz einer Eigenschaft. Ab TypeScript 4.9 hatvalueden Typobject & Record<"id", unknown>nach"id" in value.- Du musst
value !== nullprüfen, datypeof 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:
unknownist der sichere Top-Typ. Alles istunknown.neverist der Bottom-Typ.neverist ein Subtyp aller anderen Typen.anyist 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:
anyhat 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
unknownfür Daten mit unbekanntem Typ an Systemgrenzen, wende dann Narrowing mittypeof,instanceof,inoder Type Guards an. - Verwende
neverfür Funktionen, die immer Fehler werfen, für Exhaustive Checks mitassertNever, 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.