Wenige Typen in TypeScript werden so oft missverstanden wie das Quartett any, unknown, never und void. Sie sehen auf den ersten Blick wie technische Randnotizen aus, sind in Wahrheit aber die Stellschrauben, an denen sich gute von schlechter Typisierung trennt. any deaktiviert das Typsystem, unknown zwingt zu sauberer Prüfung, never markiert das Unmögliche, void beschreibt die Abwesenheit eines Rückgabewerts — und jeder dieser Typen hat sein eigenes Verhalten in der Type-Hierarchie. Dieser Artikel ordnet das Quartett ein, zeigt typische Verwechslungen und liefert die Patterns, mit denen du in der Praxis sicher arbeitest.

Das Quartett im Überblick

Die vier Typen lassen sich am besten verstehen, wenn du sie an einer einzigen Achse misst: Wieviel sagt der Typ über den Wert aus? any und unknown stehen beide an der Spitze — sie umfassen jeden möglichen Wert. Der Unterschied liegt im Schutzverhalten. never ist das genaue Gegenteil: ein Typ, der keinen einzigen Wert umfasst. Und void ist eine Sonderrolle für Funktions-Rückgaben.

TypBedeutungPositionTypischer Use Case
any„Schalte das Typsystem aus"außerhalb der HierarchieMigration aus JavaScript, letztes Mittel
unknown„Irgendein Wert, aber sicher behandelt"Top-TypeExterne Eingänge (fetch, JSON.parse)
never„Dieser Wert kann nicht existieren"Bottom-TypeExhaustiveness, nie zurückkehrende Funktion
void„Rückgabewert ist uninteressant"Funktions-RückgabeCallbacks, Side-Effect-Funktionen

Wer dieses Raster verinnerlicht, vermeidet die meisten Bugs rund um Spezial-Typen automatisch — denn fast jeder Fehler beruht darauf, dass jemand any benutzt, wo unknown korrekt wäre, oder void mit undefined verwechselt.

any — der Typsystem-Ausschalter

any ist kein „universeller Typ", sondern eine gezielte Deaktivierung aller Typprüfungen. Sobald ein Wert als any markiert ist, akzeptiert TypeScript jede Operation darauf — Property-Zugriff, Funktionsaufruf, Zuweisung in beide Richtungen.

ts any-behaviour.ts
let value: any = { x: 0 };

value.foo();          // OK
value();              // OK
value.bar = 100;      // OK
value = "hallo";      // OK
const n: number = value; // OK — alles erlaubt

Das Handbook formuliert das nüchtern: „When a value is of type any, you can access any properties of it (which will in turn be of type any), call it like a function, assign it to (or from) a value of any type, or pretty much anything else that's syntactically legal." Es gibt also keine Sicherheitsnetze mehr.

Besonders gefährlich ist das Ansteckungs-Verhalten: Jede Property eines any-Werts ist wieder any, jede Funktion, die any zurückgibt, propagiert den Typ weiter. Eine einzige any-Annotation kann sich quer durch eine Codebase ziehen, ohne dass es im Editor auffällt.

ts any-contagion.ts
function parse(input: any) {
    return input.data.items; // alles any
}

const items = parse({ data: { items: [1, 2, 3] } });
items.toUpperCase(); // Compiler sagt: OK. Laufzeit: TypeError.

Genau deshalb ist der Compiler-Flag noImplicitAny quasi Pflicht: Er erkennt jeden Punkt, an dem TypeScript mangels Annotation auf any fallen würde, und verlangt eine bewusste Entscheidung. In sauberen Codebases ist explizites any selten zulässig — meistens ist unknown oder ein konkreter Typ die bessere Wahl.

unknown — der sichere Top-Type

unknown ist der echte Top-Type des Typsystems. Jeder Wert ist unknown zuweisbar — eine string, eine Date, ein null, ein verschachteltes Objekt, alles. Der entscheidende Unterschied zu any liegt im Gegenverhalten: Aus einem unknown heraus darfst du nichts tun, solange du den Typ nicht eingegrenzt hast.

ts unknown-basics.ts
let value: unknown;

value = "hallo";       // OK
value = 42;            // OK
value = { id: 1 };     // OK

value.toUpperCase();   // Fehler: Object is of type 'unknown'.
const n: number = value; // Fehler: Type 'unknown' is not assignable.

Damit zwingt unknown dich, jeden Wert vor der Benutzung zu prüfen — typischerweise mit typeof, instanceof, in oder einem Type Predicate. Genau das wollte das Sprachdesign erreichen: Die fehlende Laufzeit-Garantie wird sichtbar statt versteckt.

ts unknown-narrowing.ts
function describe(input: unknown): string {
    if (typeof input === "string") {
        return input.toUpperCase(); // input: string
    }
    if (input instanceof Date) {
        return input.toISOString(); // input: Date
    }
    return "unbekannt";
}

In Conditional Types verhält sich unknown zudem besonders: unknown extends T ist nur dann true, wenn T selbst unknown oder any ist — unknown lässt sich also nicht unbemerkt in engere Typen pressen.

Wann any, wann unknown?

Die Faustregel ist einfach, hilft aber in fast jeder Situation:

  • any nur als kurzfristiges Werkzeug bei der Migration aus JavaScript, wenn ein ganzer Block sonst nicht compilieren würde — und auch dann mit einem TODO-Kommentar versehen.
  • unknown für alle Daten von außen: HTTP-Responses, JSON.parse, postMessage, localStorage, WebSocket-Frames, dynamische Imports.
  • Konkrete Typen in Library-Schnittstellen: Eine öffentliche API darf niemals any exponieren — das vergiftet die Nutzer-Codebase.
ts any-vs-unknown-fetch.ts
// Falsch: any verschleiert die fehlende Garantie.
async function loadBad(): Promise<{ id: number }> {
    const raw: any = await (await fetch("/api/x")).json();
    return raw; // Compiler hält still — Laufzeit kann alles sein.
}

// Richtig: unknown zwingt zur Validierung.
async function loadGood(): Promise<{ id: number }> {
    const raw: unknown = await (await fetch("/api/x")).json();
    if (
        typeof raw === "object" &&
        raw !== null &&
        "id" in raw &&
        typeof (raw as { id: unknown }).id === "number"
    ) {
        return raw as { id: number };
    }
    throw new Error("Unerwartetes API-Format");
}

In der Praxis nimmt ein Schema-Validator wie Zod dir den manuellen Narrowing-Code ab — das Prinzip bleibt aber dasselbe: Der externe Wert ist unknown, bis du ihn nachweislich überprüft hast.

never — der Bottom-Type

never ist das genaue Gegenstück zu unknown: Während unknown jeden Wert umfasst, umfasst never keinen einzigen. Der Typ existiert nur auf der Typ-Ebene und beschreibt Situationen, in denen ein Wert prinzipiell nicht entstehen kann.

Das Handbook drückt die Schlüsseleigenschaft so aus: „The never type is assignable to every type; however, no type is assignable to never (except never itself)." Damit ist never ein echter Subtyp aller Typen — der Boden der Typ-Hierarchie.

Drei typische Quellen für never:

ts never-sources.ts
// 1. Funktion, die immer wirft.
function fail(message: string): never {
    throw new Error(message);
}

// 2. Endlosschleife — kehrt nie zurück.
function loop(): never {
    while (true) {
        // ...
    }
}

// 3. Restriktive Narrowing-Folge.
function check(x: string | number) {
    if (typeof x === "string") return;
    if (typeof x === "number") return;
    // hier ist x vom Typ never — alle Optionen sind weg.
    x;
}

Der Compiler erkennt aktiv, dass throw-only-Funktionen oder echte Endlosschleifen den Typ never haben — du kannst sie überall einsetzen, wo später kein Wert mehr erwartet wird.

Exhaustiveness-Check mit never

Der praktisch wichtigste Einsatz von never ist die erschöpfende Fallunterscheidung über eine Discriminated Union. Das Prinzip: Wenn du alle Varianten in einem switch abgearbeitet hast, muss der Wert im default-Zweig never sein. Fügt jemand später eine neue Variante hinzu, schlägt genau diese Zuweisung fehl — der Compiler zwingt dich, den neuen Fall zu behandeln.

ts assert-never.ts
type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "square"; size: number }
    | { kind: "triangle"; base: number; height: number };

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

function area(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "square":
            return shape.size ** 2;
        case "triangle":
            return (shape.base * shape.height) / 2;
        default:
            return assertNever(shape);
    }
}

console.log(area({ kind: "square", size: 4 }));
Output
16

Streichst du den triangle-Fall aus dem switch, wirft der Compiler sofort einen Fehler: „Argument of type 'Triangle' is not assignable to parameter of type 'never'." Damit hast du einen statischen Erschöpfungs-Test — eine Garantie, dass kein Union-Mitglied vergessen wurde, ohne dafür einen Unit-Test schreiben zu müssen.

assertNever ist die Standardform dieses Patterns und gehört in jede TypeScript-Codebase, in der mit Discriminated Unions gearbeitet wird.

void — der „Rückgabe ignorierbar"-Typ

void ist der Typ, den TypeScript für Funktionen ohne Rückgabewert verwendet. Eine Funktion mit void-Rückgabe signalisiert: „Hier passieren Seiteneffekte, einen sinnvollen Wert gibt es nicht."

ts void-basics.ts
function log(message: string): void {
    console.log(`[LOG] ${message}`);
}

// Inferenz: Auch ohne Annotation erkennt TS hier void.
function noop() {
    return;
}

Auf den ersten Blick scheint void dasselbe zu sein wie ein expliziter undefined-Rückgabewert — schließlich gibt eine JavaScript-Funktion ohne return implizit undefined zurück. TypeScript trennt die beiden Konzepte aber bewusst, und der Unterschied wird im nächsten Abschnitt wichtig.

Wichtig ist hier vor allem: void ist kein Versprechen, dass nichts zurückkommt — sondern eine Aussage darüber, dass der Aufrufer den Rückgabewert nicht verwenden soll. Diese feine Differenz erklärt das Verhalten im Callback-Pattern.

void vs. undefined-Return

Der entscheidende Unterschied liegt im Substitution Principle für Callbacks. Eine Funktions-Typ-Annotation mit Rückgabetyp void erlaubt der konkreten Implementierung, trotzdem etwas zurückzugeben — der Wert wird einfach ignoriert. Ein Rückgabetyp undefined dagegen erzwingt, dass genau undefined (oder nichts) zurückgegeben wird.

ts void-vs-undefined.ts
type VoidFn = () => void;
type UndefinedFn = () => undefined;

const a: VoidFn = () => 42;       // OK — Rückgabe wird ignoriert.
const b: VoidFn = () => "text";   // OK — egal was, ignoriert.

const c: UndefinedFn = () => 42;        // Fehler.
const d: UndefinedFn = () => undefined; // OK.
const e: UndefinedFn = () => {};        // Fehler: implizit undefined reicht hier nicht überall.

Genau diese Regel macht das gängige Array-Pattern erst möglich:

ts foreach-pattern.ts
const source = [1, 2, 3];
const target: number[] = [];

source.forEach((n) => target.push(n));

Array.prototype.push gibt eine number zurück, forEach erwartet aber einen Callback mit Rückgabetyp void. Weil void im Kontext einer Callback-Signatur jeden Rückgabewert akzeptiert, funktioniert dieser Einzeiler — die number wird stillschweigend verworfen.

Eine wichtige Ausnahme: Wenn du eine Funktion mit einer literalen void-Annotation deklarierst (function f(): void { ... }), greift die entspannte Regel nicht. Hier ist ein expliziter return wert ein Fehler. Die Substitution gilt nur, wenn die Funktion einem void-Funktions-Typ zugewiesen wird.

Type-Hierarchie visuell

Schematisch ergibt sich folgendes Bild — never am Boden, unknown an der Spitze, alle konkreten Typen dazwischen, und any als Sonderfall daneben, der die Regeln aushebelt:

text hierarchie.txt
               unknown          ← Top-Type (sicher)

    ┌─────────────┼─────────────┐
    │             │             │
  string        number        Date  ... (konkrete Typen)
    │             │             │
    └─────────────┼─────────────┘

                never           ← Bottom-Type

    any  ────────────────────  steht außerhalb:
                               weder Top noch Bottom,
                               schaltet das System ab.

In Kurzform: never ist Subtyp jedes Typs, unknown ist Supertyp jedes Typs, und any lebt in seiner eigenen Liga — formal gilt sogar any extends unknown und unknown extends any gleichzeitig, was zeigt, dass any keinen ehrlichen Platz in der Hierarchie einnimmt.

Type-Guards um unknown zu reduzieren

In der Praxis arbeitest du sehr oft mit unknown als Eingang — und brauchst Wege, daraus einen konkreten, nutzbaren Typ herauszuschälen. TypeScript kennt vier Standard-Mechanismen, die ohne externe Library auskommen:

ts narrowing-toolkit.ts
function handle(value: unknown) {
    // 1. typeof — für Primitive.
    if (typeof value === "string") {
        return value.toUpperCase();
    }

    // 2. instanceof — für Klassen.
    if (value instanceof Error) {
        return value.message;
    }

    // 3. Array.isArray — eingebauter Guard.
    if (Array.isArray(value)) {
        return value.length;
    }

    // 4. in-Operator — Property-Test auf Objekten.
    if (typeof value === "object" && value !== null && "id" in value) {
        return (value as { id: unknown }).id;
    }

    return null;
}

Für strukturelle Prüfungen schreibst du eigene User-defined Type Predicates, deren Signatur value is T zurückgibt:

ts predicate.ts
interface User {
    id: number;
    name: string;
}

function isUser(value: unknown): value is User {
    return (
        typeof value === "object" &&
        value !== null &&
        "id" in value &&
        "name" in value &&
        typeof (value as { id: unknown }).id === "number" &&
        typeof (value as { name: unknown }).name === "string"
    );
}

function greet(input: unknown) {
    if (isUser(input)) {
        // input: User
        console.log(`Hallo, ${input.name}!`);
    }
}

Wichtig: Ein Predicate ist genauso verlässlich wie seine Implementierung. Schreibst du return true rein, vertraut TypeScript dir blind. Für kritische Pfade — vor allem alles, was aus dem Netzwerk kommt — bist du mit einem Schema-Validator wie Zod besser bedient: Das Predicate wird dort aus dem Schema generiert und ist garantiert konsistent.

Besonderheiten

any macht das Typsystem zur Doku.

Sobald ein Wert any ist, prüft der Compiler nichts mehr — die Typ-Annotationen ringsherum sind bestenfalls Kommentar. In jeder ernsthaften Codebase gehört any in Code-Reviews unter besondere Beobachtung.

unknown ist der Default für externe Daten.

JSON.parse, response.json(), postMessage, localStorage.getItem — alles, was nicht aus deinem eigenen Code stammt, wird sinnvoll als unknown angenommen, bis es validiert ist.

Array ist sicherer als any[].

unknown[] erlaubt zwar das Sammeln beliebiger Elemente, zwingt aber beim Lesen jedes einzelnen zu einem Narrowing-Schritt. any[] dagegen vergiftet jede nachfolgende Operation.

never[] ist das leere Array von etwas.

Schreibst du const xs = [], infert TypeScript zunächst never[] — ein Array, in das du nichts pushen kannst, ohne einen Typ-Hint zu geben. Genau deshalb ist const xs: number[] = [] die explizite Form.

never in Conditional Types ist distributiv.

T extends never ? A : B wirkt auf den ersten Blick einfach, verteilt sich bei Unions aber automatisch über die Mitglieder — und liefert dann oft selbst never zurück. Wer das nicht weiß, baut versehentlich Typen, die zu never kollabieren.

void in Callbacks erlaubt jeden Rückgabewert.

Eine Signatur (x: T) => void akzeptiert auch Funktionen, die etwas zurückgeben — der Wert wird nur ignoriert. Genau deshalb funktioniert arr.forEach(x => map.set(x, true)), obwohl set die Map zurückgibt.

Endlosschleifen haben den Typ never.

Schreibst du function loop(): never { while(true) {} }, erkennt der Compiler, dass die Funktion nie zurückkehrt. Dasselbe gilt für reine throw-Funktionen — beides ist Voraussetzung für den assertNever-Trick.

any und unknown sind gegenseitig assignierbar.

any extends unknown ist true — und unknown extends any ebenfalls. any ist also formal beides, was unterstreicht: Es ist kein ehrlicher Typ, sondern eine Ausnahme-Regelung im Typsystem.

Klassischer Bug: JSON.parse(...) as MyType.

Der as-Cast bedeutet null Laufzeit-Prüfung. Liefert die API ein anderes Format, fällt das erst tief im Code als undefined-Zugriff auf. Setze stattdessen auf unknown + Schema-Validator wie Zod.

void-Funktionen geben implizit undefined zurück.

Auf JavaScript-Ebene gibt jede Funktion ohne return den Wert undefined zurück. void ist trotzdem nicht dasselbe wie undefined — die entspannte Substitutions-Regel im Callback-Kontext gilt nur für void.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Primitive Typen

Zur Übersicht