type Circle = { kind: 'circle'; radius: number };
type Square = { kind: 'square'; side: number };
type Shape = Circle | Square;
Topics
26
Classes: Access Modifiers, Abstract Classes & Implements
Conditional Types
Declaration Files
Decorators
Discriminated Unions & Exhaustiveness Checking
Enums
Function Types, Overloads & Optional/Default Parameters
Generics
Index Signatures & Record Types
Mapped Types
Migrating JavaScript to TypeScript
Modules
Namespaces & Declaration Merging
Never, Unknown & Void Types
Nullability: strictNullChecks, Optional Chaining & Nullish Coalescing
Primitive Types, Arrays & Tuples
Readonly, const Assertions & Immutability
Structural Typing & Duck Typing
Template Literal Types
tsconfig & Compiler Options
Type Assertions & Type Casting
Type Inference & Contextual Typing
Type Narrowing & Guards
Types & Interfaces
Union & Intersection Types
Utility Types
Discriminated Unions & Exhaustiveness Checking
10 questions found
A discriminated union is a union of object types that all share a common literal-typed property (the discriminant, often called 'kind' or 'type'). Checking that property's value in an if or switch lets TypeScript automatically narrow the whole object to the matching specific type.
Real-world example
Modeling different states of an async request (loading, success, error) as a discriminated union.
Union & Intersection Types
Switch on the discriminant property; inside each case block, TypeScript automatically narrows the variable to the specific variant matching that case's literal value, giving you safe access to that variant's unique properties.
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2; // shape is Circle here
case 'square': return shape.side ** 2; // shape is Square here
}
}
Real-world example
Calculating an area, rendering a UI component, or serializing a response differently based on a variant's specific shape.
Type Narrowing & Guards
What is exhaustiveness checking, and how does the 'never' type enable it in a default case?
IntermediateExhaustiveness checking uses the fact that after handling every known variant, the remaining type should be 'never' (impossible) — assigning the narrowed value to a variable typed as never in a default/else branch causes a compile error if a new variant is ever added and forgotten, catching incomplete handling at compile time.
function assertNever(x: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle': return Math.PI * shape.radius ** 2;
case 'square': return shape.side ** 2;
default: return assertNever(shape); // error here if a case is missing
}
}
Real-world example
Guaranteeing that adding a new Shape variant later forces every switch statement handling shapes to be updated, caught by the compiler.
Never
Unknown & Void Types
How would you model API response states (loading, success, error) as a discriminated union?
IntermediateDefine a union where each state variant has the same discriminant key (e.g. 'status') with a different literal value, plus only the data relevant to that specific state — this prevents accessing 'data' before checking status is 'success', or 'error' before checking status is 'error'.
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function render(state: RequestState<User>) {
if (state.status === 'success') console.log(state.data); // safe access
}
Real-world example
Modeling a data-fetching hook's state so consuming components can't accidentally access 'data' while still loading.
Union & Intersection Types
Can you use a discriminant that isn't a string literal, like a number or boolean literal type?
IntermediateYes — the discriminant just needs to be a literal type (string, number, or boolean literal) that's unique across the union's variants; TypeScript narrows equally well whether you check with ===, a switch, or even truthy/falsy checks on a boolean discriminant.
type Success = { ok: true; data: string };
type Failure = { ok: false; error: string };
type Result = Success | Failure;
function handle(result: Result) {
if (result.ok) console.log(result.data); // Success
else console.log(result.error); // Failure
}
Real-world example
Using a simple boolean 'ok' discriminant for a lightweight Result<T> type, common in functional-style error handling.
Types & Interfaces
How do you write a generic Result<T, E> discriminated union type for functional-style error handling, and why is it preferred over throwing exceptions in some codebases?
AdvancedDefine a generic union of Ok<T> and Err<E> variants with a common discriminant; unlike throw, which is invisible in a function's type signature, a Result<T, E> return type makes the possibility of failure explicit and forces callers to handle it via the type system rather than relying on documentation or try/catch discipline.
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseNumber(input: string): Result<number, string> {
const n = Number(input);
return isNaN(n) ? { ok: false, error: 'Invalid number' } : { ok: true, value: n };
}
Real-world example
Making error handling explicit and type-checked in a validation or parsing layer, rather than relying on undocumented thrown exceptions.
Generics
How does exhaustiveness checking behave differently if the union member types overlap structurally instead of being cleanly discriminated?
AdvancedIf variants don't share a unique literal discriminant (or their shapes overlap ambiguously), narrowing based on property checks becomes unreliable — TypeScript may not be able to distinguish which variant you're in, weakening or breaking exhaustiveness checking entirely, since the 'never' trick relies on the compiler correctly eliminating all known variants.
// Poorly discriminated - both have 'value: number', ambiguous
type A = { type: 'a'; value: number };
type B = { value: number }; // no matching 'type' discriminant at all
type Bad = A | B; // narrowing on 'type' alone won't safely narrow B
Real-world example
Debugging why a switch/case exhaustiveness check silently fails to catch a missing case, tracing it to a poorly-discriminated union.
Union & Intersection Types
How would you write a reducer function (Redux-style) that exhaustively handles every action type in a discriminated union of actions?
AdvancedType the reducer's action parameter as a discriminated union of all possible Action types (each with a unique 'type' discriminant), switch on action.type, and add a default case using the assertNever(action) pattern — this guarantees the compiler flags any new action type added later that isn't yet handled in the reducer.
type Action =
| { type: 'increment'; amount: number }
| { type: 'decrement'; amount: number }
| { type: 'reset' };
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'increment': return state + action.amount;
case 'decrement': return state - action.amount;
case 'reset': return 0;
default: return assertNever(action);
}
}
Real-world example
Building a type-safe Redux or useReducer reducer where forgetting to handle a new action type is caught at compile time, not at runtime.
Never
Unknown & Void Types
How do you nest discriminated unions to model a state machine with multiple independent dimensions of state?
AdvancedCombine multiple discriminants (or nest one discriminated union inside a variant of another) when a single state genuinely has more than one independent axis of variation — though often it's cleaner to flatten everything into a single discriminant enumerating every valid COMBINATION of states, avoiding impossible in-between states entirely.
type FetchState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: string; cached: boolean }
| { status: 'error'; error: string; retryable: boolean };
// 'cached' and 'retryable' only exist on their relevant variant, preventing impossible combinations
Real-world example
Modeling a network request state machine where 'cached' only makes sense once data has successfully loaded.
Union & Intersection Types
How does TypeScript narrow a discriminated union when the discriminant check happens inside a called function rather than inline?
AdvancedBy default, TypeScript does NOT narrow based on an external function's return value unless that function is declared as a user-defined type predicate (using the 'x is Type' return type syntax) — a plain boolean-returning helper won't narrow the union at the call site, even if its logic is equivalent to an inline check.
function isCircle(shape: Shape): shape is Circle {
return shape.kind === 'circle'; // type predicate enables narrowing
}
function badIsCircle(shape: Shape): boolean {
return shape.kind === 'circle'; // plain boolean does NOT narrow
}
if (isCircle(shape)) { shape.radius; } // works
// if (badIsCircle(shape)) { shape.radius; } // Error: radius doesn't exist on Shape
Real-world example
Extracting a reusable, well-typed 'isX' check into a shared utility function used across multiple switch/if statements.
Type Narrowing & Guards