Discriminated Unions — auch Tagged Unions genannt — sind das mit Abstand wichtigste Strukturmuster, das TypeScript für die Modellierung von Zuständen bietet. Statt ein einzelnes Interface mit lauter optionalen Feldern zu pflastern, das in der Hälfte der Fälle die falschen Properties enthält, erzeugst du mehrere kleine, in sich konsistente Typen und verbindest sie über ein einziges Literal-Property zu einer Union. Der Compiler erkennt dieses Discriminator-Property automatisch und engt den Typ in jedem Zweig deines Codes präzise auf einen einzelnen Union-Member ein — ohne Cast, ohne Type-Assertion, ohne Vertrauensvorschuss. Wer dieses Pattern verinnerlicht, schreibt API-Layer, Reducer und Form-Logik mit einer Sicherheit, die in dynamischen Sprachen schlicht nicht erreichbar ist. Dieser Artikel zeigt dir das Pattern an mehreren praxisrelevanten Beispielen, beleuchtet den Exhaustiveness-Check mit never und benennt die typischen Anti-Patterns, die du in fremdem Code immer wieder sehen wirst.

Das Problem ohne Discriminator

Stell dir vor, du modellierst den Zustand eines asynchronen Requests. Drei Fälle sind möglich: er lädt gerade, er hat erfolgreich Daten geliefert, oder er ist fehlgeschlagen. Ein naiver erster Versuch bündelt alles in einem einzigen Interface mit lauter optionalen Feldern:

ts naiv.ts
// Anti-Pattern — ein Interface für alle Fälle.
interface AsyncState {
    loading?: boolean;
    data?: unknown;   // ggf. da, ggf. nicht
    error?: string;   // ggf. da, ggf. nicht
}

function render(state: AsyncState): string {
    if (state.loading) {
        return "laedt ...";
    }
    // Problem 1: state.data ist `unknown | undefined` — du musst defensiv pruefen.
    // Problem 2: nichts hindert dich, gleichzeitig data UND error zu setzen.
    // Problem 3: der Compiler kann nicht beweisen, dass im Erfolgsfall data existiert.
    if (state.error) {
        return `Fehler: ${state.error}`;
    }
    return `Daten: ${JSON.stringify(state.data)}`; // data koennte undefined sein
}

Drei reale Schmerzpunkte stecken in diesem Code. Erstens: die invarianten Zustände sind nicht ausgeschlossen — ein Objekt mit loading: true, error: "x" wird vom Typsystem akzeptiert, obwohl das semantisch Unsinn ist. Zweitens: jeder Zugriff auf data oder error braucht eine defensive Null-Prüfung, weil der Compiler nicht weiß, dass die Felder im jeweiligen Erfolgs- oder Fehlerfall garantiert da sind. Drittens: beim Refactoring — etwa beim Hinzufügen eines neuen Zustands "retrying" — gibt es keine Compile-Hilfe, die dir zeigt, welche Stellen du anpassen musst.

Die Lösung ist nicht „mehr Optional-Felder hinzufügen" und auch nicht „Runtime-Asserts schreiben". Die Lösung ist eine strukturelle Umorganisation: aus einem fetten Interface werden mehrere schmale, die per Discriminator zu einer Union verbunden sind.

Was ein Discriminator ist

Ein Discriminator — auch Tag oder Discriminant genannt — ist ein gemeinsames Property, das in jedem Mitglied einer Union vorkommt und dort einen eindeutigen Literal-Typ trägt. Wichtig sind drei Eigenschaften:

  • Es muss in jedem Union-Member existieren (nicht optional).
  • Es muss ein Literal-Typ sein ("loading", "success", 42, true), kein offener string.
  • Jeder Member benutzt einen anderen Literal-Wert.

Ist diese Konstellation erfüllt, erkennt TypeScript das Property automatisch als Discriminator und narrowt die Union beim Vergleich mit ===, im switch, in if-Branches oder via destructuring.

ts discriminator.ts
// Drei Member, jeder mit eindeutigem `kind`-Literal als Tag.
interface Loading { kind: "loading" }
interface Success { kind: "success"; data: string }
interface Failure { kind: "failure"; message: string }

type AsyncState = Loading | Success | Failure;

function describe(s: AsyncState): string {
    // Vergleich mit Literal — Compiler narrowt automatisch.
    if (s.kind === "success") {
        return s.data; // s ist hier garantiert Success
    }
    return "kein Erfolg";
}

Das ist der gesamte konzeptuelle Kern. Alles, was im Rest dieses Artikels folgt, ist eine Variation dieses Musters für unterschiedliche Domänen.

Erstes vollständiges Beispiel: AsyncState<T>

Wir bauen das Async-State-Beispiel jetzt sauber auf, generisch über die Payload T, und durchziehen es mit einem switch-Renderer. Das ist eine direkte Vorlage für jedes UI, das auf Daten wartet.

ts async-state.ts
// Drei Zustaende — jeder mit den exakt passenden Begleit-Daten.
type AsyncState<T> =
    | { kind: "idle" }                          // noch nicht gestartet
    | { kind: "loading" }                       // Request laeuft
    | { kind: "success"; data: T }              // Erfolg mit Payload
    | { kind: "failure"; error: Error };        // Fehler mit Error-Objekt

// Render-Funktion über switch — jeder Case sieht nur "seine" Felder.
function renderUser(state: AsyncState<{ name: string }>): string {
    switch (state.kind) {
        case "idle":
            return "Noch nicht geladen.";
        case "loading":
            return "Lade Benutzer ...";
        case "success":
            // state ist hier { kind: "success"; data: { name: string } }
            return `Benutzer: ${state.data.name}`;
        case "failure":
            // state.error ist garantiert vom Typ Error.
            return `Fehler: ${state.error.message}`;
    }
}

console.log(renderUser({ kind: "idle" }));
console.log(renderUser({ kind: "loading" }));
console.log(renderUser({ kind: "success", data: { name: "Anna" } }));
console.log(renderUser({ kind: "failure", error: new Error("Timeout") }));
Output
Noch nicht geladen.
Lade Benutzer ...
Benutzer: Anna
Fehler: Timeout

Drei Dinge sind an diesem Ausschnitt bemerkenswert. Erstens: im case "success" ist state.data ohne weitere Prüfung verfügbar — der Compiler hat den Typ auf den Success-Member verengt und weiß, dass data da ist. Zweitens: ein Aufruf wie renderUser(&#123; kind: "success" &#125;) ohne data wird als Compile-Fehler abgelehnt, weil die Form nicht passt. Drittens: dieselbe Struktur funktioniert beliebig generisch — AsyncState&lt;Order&gt;, AsyncState&lt;Order[]&gt;, AsyncState&lt;{ token: string &#125;&gt; sind alle frei kombinierbar.

Exhaustiveness-Check mit never

Was passiert, wenn du später einen vierten Zustand "retrying" hinzufügst, aber vergisst, ihn in einem der vielen Renderer zu behandeln? Ohne Vorkehrung passiert genau nichts — der Code kompiliert, und der vergessene Fall fällt durch alle Cases und liefert undefined. Genau hier setzt der Exhaustiveness-Check an.

Das Werkzeug dafür ist eine Helper-Funktion, die den Typ never als Parameter verlangt. never ist der leere Typ: kein einziger Wert ist ihm zuweisbar. Wenn der Compiler im default-Zweig eines switch feststellt, dass dort noch ein „echter" Typ ankommt, weigert er sich, ihn als never zu akzeptieren — und du bekommst einen exakten Fehlerhinweis.

ts exhaustive.ts
// Helper — wirft zur Laufzeit und ist zur Compile-Zeit der Exhaustiveness-Wachhund.
function assertNever(x: never): never {
    throw new Error(`Unerwarteter Wert: ${JSON.stringify(x)}`);
}

type AsyncState =
    | { kind: "idle" }
    | { kind: "loading" }
    | { kind: "success"; data: string }
    | { kind: "failure"; error: Error };

function render(state: AsyncState): string {
    switch (state.kind) {
        case "idle":     return "idle";
        case "loading":  return "loading";
        case "success":  return state.data;
        case "failure":  return state.error.message;
        default:
            // Sobald hier ein nicht-behandelter Member ankommt, schreit der Compiler.
            return assertNever(state);
    }
}

Solange die Union komplett abgedeckt ist, betrachtet TypeScript das state im default-Zweig als never — der Aufruf von assertNever(state) ist zulässig. Sobald du aber einen neuen Member hinzufügst — etwa &#123; kind: "retrying"; attempt: number &#125; — und vergisst, einen case dafür zu ergänzen, gibt es im default einen sehr klaren Fehler:

ts exhaustive-broken.ts
// Annahme: Union wurde um { kind: "retrying"; attempt: number } erweitert,
// der switch oben aber NICHT angepasst.

// Compiler-Fehler im default-Zweig:
//   Argument of type '{ kind: "retrying"; attempt: number; }'
//   is not assignable to parameter of type 'never'.

// Effekt: du kommst NICHT durch den Build, ohne den neuen Case zu behandeln.

Das ist der entscheidende Mehrwert gegenüber einem nackten switch ohne default: dort würde der Code stumm weiterlaufen, hier brennt die Hütte im CI. Genau dieses Verhalten willst du, wenn dein State-Modell für die Korrektheit deiner App zentral ist. Faustregel: jedes switch über einen Discriminator bekommt einen default-Zweig mit assertNever.

API-Response-Modellierung

REST-APIs liefern in der Realität nicht einfach „Daten oder nichts", sondern entweder eine erfolgreiche Antwort mit Payload oder eine strukturierte Fehlerantwort mit Code, Message und vielleicht Validation-Details. Das ist ein Lehrbuch-Anwendungsfall für eine Discriminated Union.

ts api-response.ts
// Generischer Response-Container über die Payload T.
type ApiResponse<T> =
    | { status: "ok"; data: T; meta?: { traceId: string } }
    | { status: "error"; code: number; message: string; fields?: Record<string, string> };

interface User {
    id: number;
    name: string;
}

// Eine fingierte fetch-Funktion. In Echt: response = await fetch(...).json()
function fetchUser(id: number): ApiResponse<User> {
    if (id < 0) {
        return {
            status: "error",
            code: 400,
            message: "Ungueltige ID",
            fields: { id: "muss positiv sein" },
        };
    }
    return {
        status: "ok",
        data: { id, name: "Anna" },
        meta: { traceId: "abc-123" },
    };
}

function handle(res: ApiResponse<User>): void {
    if (res.status === "ok") {
        // res hat hier garantiert .data und optional .meta — kein .code, kein .message.
        console.log(`OK: ${res.data.name} (trace ${res.meta?.traceId ?? "-"})`);
        return;
    }
    // Im else-Zweig narrowt TS auf den Error-Member.
    console.log(`ERR ${res.code}: ${res.message}`);
    if (res.fields) {
        for (const [k, v] of Object.entries(res.fields)) {
            console.log(`  ${k}: ${v}`);
        }
    }
}

handle(fetchUser(1));
handle(fetchUser(-1));
Output
OK: Anna (trace abc-123)
ERR 400: Ungueltige ID
  id: muss positiv sein

Beachte die scharfe Trennung der Begleit-Felder: der Erfolgsfall trägt data und optional meta, der Fehlerfall trägt code, message und optional fields. Es gibt keine Überschneidung, keine optionalen Daten-Felder, keine if (res.data)-Sicherheits-Tänze. Wer auf res.code zugreifen will, muss vorher beweisen, dass es sich um einen Error handelt — und das ist genau der Sinn der Übung.

Reducer-Actions (Redux-Style)

Das vielleicht klassischste Discriminated-Union-Beispiel überhaupt: Reducer-Actions in Redux, Zustand, useReducer und allen verwandten State-Bibliotheken. Eine Action ist konzeptuell exakt das, was eine Discriminated Union ausdrücken soll: ein Objekt mit einem festen Typ-Marker und einem zum Marker passenden Payload.

ts reducer.ts
interface Todo {
    id: number;
    text: string;
    done: boolean;
}

type State = { todos: Todo[] };

// Drei Actions — der Discriminator heisst hier `type` (Redux-Konvention).
type Action =
    | { type: "todo/add"; text: string }
    | { type: "todo/remove"; id: number }
    | { type: "todo/toggle"; id: number };

function assertNever(x: never): never {
    throw new Error(`Unbekannte Action: ${JSON.stringify(x)}`);
}

function reducer(state: State, action: Action): State {
    switch (action.type) {
        case "todo/add":
            // action.text ist hier verfuegbar; action.id existiert nicht.
            return {
                todos: [...state.todos, { id: Date.now(), text: action.text, done: false }],
            };
        case "todo/remove":
            // action.id ist verfuegbar; action.text existiert nicht.
            return { todos: state.todos.filter(t => t.id !== action.id) };
        case "todo/toggle":
            return {
                todos: state.todos.map(t =>
                    t.id === action.id ? { ...t, done: !t.done } : t
                ),
            };
        default:
            return assertNever(action);
    }
}

let s: State = { todos: [] };
s = reducer(s, { type: "todo/add", text: "Milch kaufen" });
s = reducer(s, { type: "todo/add", text: "Brot kaufen" });
s = reducer(s, { type: "todo/toggle", id: s.todos[0].id });
console.log(s.todos.map(t => `${t.done ? "[x]" : "[ ]"} ${t.text}`).join("\n"));
Output
[x] Milch kaufen
[ ] Brot kaufen

Der Compiler erzwingt drei Dinge gleichzeitig: dass jede Action im Aufruf ihre Pflichtfelder mitliefert (text bei add, id bei remove/toggle), dass im Reducer-Body nur die zum Action-Typ passenden Felder zugreifbar sind, und dass kein Action-Typ vergessen werden kann, sobald du assertNever einsetzt. In Codebases mit Dutzenden von Actions ist das ein massiver Vorteil gegenüber stringly-typed Redux à la 2017.

Form-States

Formulare sind ein zweiter idealer Anwendungsfall: ein Formular ist nicht einfach „leer oder voll", sondern hat einen Lebenszyklus aus idle, submitting, submitted und failed — und jeder dieser Zustände trägt eigene Begleitdaten. Im submitting-Zustand kennst du etwa schon die Eingaben, aber noch keinen Receipt; im submitted-Zustand kennst du den Receipt; im failed-Zustand kennst du die Fehler pro Feld.

ts form-state.ts
interface ContactInput {
    name: string;
    email: string;
    message: string;
}

interface Receipt {
    ticketId: string;
    createdAt: string;
}

// Vier sauber getrennte Zustaende — jeder mit exakt den Daten, die er trotzdem braucht.
type FormState =
    | { phase: "idle" }                                              // Formular leer
    | { phase: "submitting"; values: ContactInput }                  // POST laeuft
    | { phase: "submitted"; values: ContactInput; receipt: Receipt } // Erfolg
    | { phase: "failed"; values: ContactInput;
        fieldErrors: Partial<Record<keyof ContactInput, string>> }; // Validierung

function statusLabel(s: FormState): string {
    switch (s.phase) {
        case "idle":
            return "Bereit.";
        case "submitting":
            // values vorhanden, receipt/fieldErrors nicht.
            return `Sende für ${s.values.email} ...`;
        case "submitted":
            return `Erledigt — Ticket ${s.receipt.ticketId}`;
        case "failed":
            // Wir wissen sicher, dass fieldErrors existiert.
            const count = Object.keys(s.fieldErrors).length;
            return `Fehlgeschlagen mit ${count} Validierungs-Fehler(n).`;
    }
}

Schau dir das Pattern bewusst an: der Zustand „bereits gesendet, aber Receipt liegt vor" trägt sowohl die Original-values als auch den receipt. Eine optional-Felder-Lösung würde hier zu vier optional-flags und endlosen Defensiv-Checks führen. Mit der Discriminated Union schreibst du genau einmal die Form auf, und der Compiler übernimmt den Rest.

Best Practice — Discriminator-Property-Konventionen

In freier Wildbahn siehst du verschiedene Schreibweisen für den Discriminator: kind, type, tag, _tag, discriminator, sogar __typename (GraphQL). Aus TypeScript-Sicht ist der Name technisch egal — solange jedes Union-Member dasselbe Property mit einem Literal-Typ trägt, funktioniert das Narrowing. Aus Lesbarkeits- und Konsistenzsicht ist es aber nicht egal.

PropertyVerbreitungHinweis
kindTS-Handbook, viele LibsEmpfohlene Default-Wahl — kurz und neutral
typeRedux, FluxStandardActionStandard in Reducer-Welten; kollidiert mit JS-typeof-Konzept
tag / _tagfp-ts, effect-tsIn funktionaler Welt verbreitet (Haskell-Erbe)
__typenameGraphQL-CodegenGesetzt durch Apollo/urql; akzeptieren, nicht selbst erfinden
statusAPI-ResponsesInhaltlich passend bei Erfolg/Fehler-Trennung
phaseWorkflow-/Form-StatesSelbstdokumentierend bei Lifecycle-Zustaenden

Faustregel für ein neues Projekt: einigt euch einmal auf kind oder type und zieht das durch. Inhaltlich präzisere Namen (status, phase) sind erlaubt, wenn sie dem Leser sofort die Domäne klarmachen — aber dann konsistent innerhalb des Moduls. Der schlimmste Stil ist „mal kind, mal type, mal status, je nach Tageslaune".

Anti-Patterns

Discriminated Unions sind so einfach, dass die häufigsten Fehler nicht im Pattern selbst, sondern in kleinen Abweichungen liegen, die das Narrowing brechen oder unnötig einschränken.

Anti-Pattern 1: Boolean als Discriminator. Es funktioniert technisch — true und false sind Literal-Typen — ist aber nicht erweiterbar. Sobald ein dritter Zustand auftaucht, musst du das gesamte Modell umbauen.

ts anti-boolean.ts
// Funktioniert, aber sackgasse:
type Result =
    | { isError: false; value: string }
    | { isError: true; message: string };

// Sobald du einen dritten Fall brauchst (z. B. "loading"), passt kein boolean mehr.
// Besser von Anfang an mit string-Literalen:
type ResultBetter =
    | { kind: "success"; value: string }
    | { kind: "failure"; message: string };

Anti-Pattern 2: Optionaler Discriminator. Sobald der Tag ?-optional ist, kann er undefined sein — und das Narrowing bricht zusammen. Ein Discriminator ist Pflichtfeld, immer.

ts anti-optional.ts
// BROKEN — optionaler Discriminator.
type Bad =
    | { kind?: "a"; a: number }
    | { kind?: "b"; b: number };

function f(x: Bad) {
    if (x.kind === "a") {
        // TS kann hier nicht sicher narrowen — `kind` koennte auch im b-Member fehlen.
        // x.a koennte daher undefined sein.
    }
}

Anti-Pattern 3: String statt Literal. Wenn der Discriminator als offener string typisiert ist (etwa weil er aus JSON.parse kommt und nicht über Zod o.ä. validiert wurde), funktioniert kein Narrowing — TypeScript hat keine Anhaltspunkte, welcher Member gemeint sein könnte.

ts anti-string.ts
// BROKEN — Discriminator ist `string`, nicht Literal.
type AlsoBad =
    | { kind: string; a: number }   // <- string ist viel zu weit
    | { kind: string; b: number };

// Compiler hat hier nichts zu unterscheiden.
// Loesung: immer Literal-Typen verwenden ("a", "b" als Werte, nicht string).

Anti-Pattern 4: Mehrere Discriminatoren parallel. Theoretisch erlaubt, praktisch fast nie sinnvoll. Wer kind und type parallel verwendet, doppelt die Verzweigungs-Logik und sorgt für inkonsistente Werte-Kombinationen. Nimm einen Discriminator und bleibe dabei. Wenn du eine zusätzliche Achse modellieren musst, ist meistens eine verschachtelte Union die saubere Antwort (siehe Abschnitt 10).

Anti-Pattern 5: Optionale Felder im Member. Wenn data im Success-Member als data?: T deklariert ist, hast du nichts gewonnen — du brauchst weiterhin einen Null-Check. Die ganze Idee ist, dass im jeweiligen Member die zugehörigen Daten garantiert vorhanden sind.

Type Guards für Discriminated Unions

switch und if-Vergleiche reichen für 95 % aller Fälle. In den verbleibenden Fällen — typisch: ein Helper, der nur eine bestimmte Variante akzeptiert, oder ein filter()-Aufruf, der das Array-Element verengen soll — sind Custom Type Guards der saubere Weg.

ts type-guard.ts
type AsyncState<T> =
    | { kind: "idle" }
    | { kind: "loading" }
    | { kind: "success"; data: T }
    | { kind: "failure"; error: Error };

// Custom Type Guard — Rueckgabetyp `state is { ... }` ist der entscheidende Trick.
function isSuccess<T>(state: AsyncState<T>): state is { kind: "success"; data: T } {
    return state.kind === "success";
}

const states: AsyncState<number>[] = [
    { kind: "idle" },
    { kind: "loading" },
    { kind: "success", data: 42 },
    { kind: "failure", error: new Error("x") },
    { kind: "success", data: 99 },
];

// Dank des Type Guard ist `successes` korrekt als Success-Array typisiert.
const successes = states.filter(isSuccess);
// successes: { kind: "success"; data: number }[]

console.log(successes.map(s => s.data).reduce((a, b) => a + b, 0));
Output
141

Der Knackpunkt ist die Type Predicate state is { kind: "success"; data: T &#125; als Rückgabetyp. Ohne sie würde filter() den Typ nicht verengen, weil die Standard-Signatur von Array#filter für boolesche Prädikate nur T[] zurückgibt. Mit ihr schenkt dir TypeScript die saubere Verengung — und du sparst dir einen as-Cast, der genau das Sicherheitsversprechen aushebeln würde.

Für verschachtelte Discriminated Unions — z. B. ein FormState, dessen submitted-Member intern wieder eine ApiResponse&lt;Receipt&gt; enthält — kannst du Type Guards beliebig kombinieren oder einfach in zwei switch-Ebenen aufsplitten. Beide Ansätze sind sauber; entscheide nach Lesbarkeit, nicht nach Dogma.

Besonderheiten

Tagged Union stammt aus der funktionalen Welt.

Der Begriff kommt aus Haskell und ML, wo solche Typen als data Result a = Ok a | Err String direkt im Sprachkern verankert sind. TypeScript bildet dasselbe Muster mit Union- und Literal-Typen nach — ohne eigene Syntax dafuer, aber mit demselben Ausdrucks-Potenzial.

TypeScript narrowt automatisch — kein Cast noetig.

Sobald der Compiler ein Literal-Property erkennt und einen Vergleich darauf sieht, engt er den Typ in dem entsprechenden Branch von selbst ein. Wer dort noch as schreibt, hat das Pattern nicht verstanden oder kaempft mit einem anderen Bug.

Exhaustiveness-Check faengt vergessene Cases zur Compile-Zeit.

Mit assertNever im default-Zweig wird jedes Hinzufuegen eines neuen Union-Members zum erzwungenen Refactoring-Schritt. Genau dieser Mechanismus macht grosse State-Maschinen ueberhaupt erst wartbar.

kind ist die haeufigste Discriminator-Property in der TS-Community.

Das offizielle TS-Handbook benutzt durchgaengig kind, und Bibliotheken wie ts-pattern oder Beispiele in TS-Talks orientieren sich daran. Wer keinen guten Grund hat, sollte mitziehen.

Reducer in Redux, Zustand und useReducer profitieren massiv.

Vor Discriminated Unions waren Reducer in Redux-Apps eine endlose Quelle von action.payload.someField-Tippfehlern. Heute ist eine Action-Union mit type-Discriminator der absolute Standard — bis hin zu Codegen-Tools, die genau dieses Pattern produzieren.

Discriminated Unions koennen verschachtelt werden.

Ein FormState kann im submitted-Member eine ApiResponse<T> halten, die wiederum eine eigene Discriminated Union ist. Du kannst beliebig tief stapeln — der Compiler narrowt auf jeder Ebene mit. Die einzige Grenze ist Lesbarkeit.

Zod, Valibot und ArkType unterstuetzen Discriminated Unions als Schema.

Statt z.union([...]) mit teurer All-Branch-Validierung gibt es z.discriminatedUnion("kind", [...]), das den Tag liest und direkt zur passenden Schema-Variante springt. Spuerbar schneller und mit besseren Fehlermeldungen — die Schema-Bibliotheken nutzen also exakt dasselbe strukturelle Argument wie der TS-Compiler.

Sehr grosse Discriminated Unions sind compile-bremsen-anfaellig.

Ab mehreren hundert Membern oder bei komplexen Generics werden Type-Inferenz-Schritte teuer. Wer Action-Unions mit dreistelliger Mitgliederzahl pflegt, sollte auf Project References, skipLibCheck und ggf. Zerlegung in Sub-Unions setzen — und nicht alles in eine Riesen-Union pressen.

Faustregel: jedes status- oder state-Feld ist ein Discriminator-Kandidat.

Sobald in einem Interface ein String-Feld mit endlicher, semantisch geladener Wertemenge auftaucht ("draft" | "published" | "archived"), lohnt sich die Frage: koennte das eine Discriminated Union sein, bei der jeder Status andere Begleitdaten hat? Die Antwort ist erstaunlich oft ja.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Union & Intersection

Zur Übersicht