Discriminated unions, two ways
string literals vs. classes.
Model a type that can be one of several distinct shapes — and have TypeScript narrow it correctly based on a discriminant field. Two implementations: string literal unions and class hierarchies.
The Problem
You have an API response that’s either a success or an error. Naive typing:
type Result = { data?: User; error?: string; ok: boolean;};TypeScript can’t tell you that data is defined when ok is true. You end up with data! non-null assertions or runtime checks.
Version 1: String Literal Discriminated Union
type Result<T> = { ok: true; data: T } | { ok: false; error: string };
function handle(result: Result<User>) { if (result.ok) { console.log(result.data.name); // TypeScript knows data exists } else { console.log(result.error); // TypeScript knows error exists }}The ok field is the discriminant. TypeScript narrows the type inside each branch.
Version 2: Class Hierarchy
class Success<T> { readonly ok = true as const; constructor(readonly data: T) {}}
class Failure { readonly ok = false as const; constructor(readonly error: string) {}}
type Result<T> = Success<T> | Failure;
function handle(result: Result<User>) { if (result instanceof Success) { console.log(result.data.name); } else { console.log(result.error); }}instanceof is the discriminant here. Classes add methods if you need them.
When to Use Each
Use string literal unions when the type is data-only and you want to keep it serializable. Use classes when you need methods on the variants or want instanceof narrowing.
Why It Matters
Discriminated unions eliminate a whole class of runtime bugs. When TypeScript can prove which variant you have, you stop writing defensive null checks and start writing code that assumes what it already knows.