0
0
Typescriptprogramming~15 mins

Discriminated unions in Typescript - Deep Dive

Choose your learning style9 modes available
Overview - Discriminated unions
What is it?
Discriminated unions are a way to combine multiple types into one, where each type has a special property that tells them apart. This special property is called the discriminator. It helps TypeScript know exactly which type you are working with at any time. This makes your code safer and easier to understand.
Why it matters
Without discriminated unions, you would have to guess or manually check what kind of data you have, which can cause bugs and confusion. Discriminated unions let TypeScript automatically figure out the type based on the special property, so you get helpful errors and better code completion. This saves time and prevents mistakes in real projects.
Where it fits
Before learning discriminated unions, you should know basic TypeScript types, interfaces, and union types. After this, you can learn about advanced type narrowing, type guards, and pattern matching to write even more precise and safe code.
Mental Model
Core Idea
Discriminated unions use a shared special property to tell different types apart so TypeScript can safely choose the right one.
Think of it like...
Imagine a box of different fruits where each fruit has a label saying what it is. By reading the label, you know exactly if it's an apple, banana, or orange without guessing.
┌─────────────────────────────┐
│       Discriminated Union    │
├─────────────┬───────────────┤
│  Type A     │  Type B       │
├─────────────┼───────────────┤
│ kind: 'a'   │ kind: 'b'     │
│ propA: ...  │ propB: ...    │
└─────────────┴───────────────┘

TypeScript checks 'kind' to know if it's Type A or B.
Build-Up - 7 Steps
1
FoundationUnderstanding union types basics
🤔
Concept: Learn what union types are and how they let a value be one of several types.
In TypeScript, a union type means a value can be one type or another. For example: let value: string | number; value = 'hello'; // okay value = 42; // okay But TypeScript doesn't know which one it is at any moment without extra checks.
Result
You can store different types in one variable, but TypeScript can't tell which one it is without help.
Understanding union types is the first step to knowing why discriminated unions are useful for safely working with multiple types.
2
FoundationIntroducing type narrowing with checks
🤔
Concept: Learn how to tell TypeScript which type you have by checking properties or using typeof.
When you have a union, you can use if statements to check the type: if (typeof value === 'string') { // TypeScript knows value is string here } else { // Here value is number } This is called type narrowing.
Result
TypeScript can safely let you use properties or methods specific to the narrowed type.
Type narrowing is how TypeScript understands which type you mean inside code blocks, making unions safer to use.
3
IntermediateCreating discriminated unions with a common property
🤔Before reading on: do you think a shared property with different values can help TypeScript pick the right type automatically? Commit to yes or no.
Concept: Learn to add a shared property with unique values to each type to help TypeScript discriminate between them.
Instead of just union types, add a common property called a discriminator: interface Square { kind: 'square'; size: number; } interface Circle { kind: 'circle'; radius: number; } type Shape = Square | Circle; Now 'kind' tells TypeScript which shape it is.
Result
TypeScript can use the 'kind' property to know exactly which type you have when you check it.
Using a shared property with unique values lets TypeScript automatically narrow types without guessing.
4
IntermediateUsing discriminated unions in switch statements
🤔Before reading on: do you think switch statements can fully narrow discriminated unions without extra type assertions? Commit to yes or no.
Concept: Learn how switch statements on the discriminator property let TypeScript know the exact type in each case.
Using the Shape example: function area(shape: Shape) { switch (shape.kind) { case 'square': return shape.size * shape.size; case 'circle': return Math.PI * shape.radius ** 2; } } TypeScript knows inside each case what properties are safe to use.
Result
You get safe, clear code that handles each type correctly with no errors.
Switching on the discriminator is the cleanest way to handle all cases safely and clearly.
5
IntermediateExtending discriminated unions with more types
🤔
Concept: Learn how to add more types to the union by giving each a unique discriminator value.
Add a new shape: interface Rectangle { kind: 'rectangle'; width: number; height: number; } type Shape = Square | Circle | Rectangle; Update the switch: case 'rectangle': return shape.width * shape.height; TypeScript still narrows correctly.
Result
You can safely expand your union with new types without breaking existing code.
Discriminated unions scale well by adding new types with unique discriminators.
6
AdvancedExhaustiveness checking with never type
🤔Before reading on: do you think TypeScript can warn you if you forget to handle a case in a discriminated union? Commit to yes or no.
Concept: Learn how to make TypeScript check that all union cases are handled using the never type.
Add a default case that never happens: function area(shape: Shape) { switch (shape.kind) { case 'square': return shape.size * shape.size; case 'circle': return Math.PI * shape.radius ** 2; case 'rectangle': return shape.width * shape.height; default: const _exhaustiveCheck: never = shape; return _exhaustiveCheck; } } If you forget a case, TypeScript shows an error.
Result
You get compile-time safety that all cases are handled, preventing bugs.
Exhaustiveness checking ensures your code stays correct as you add new types.
7
ExpertDiscriminated unions with complex nested types
🤔Before reading on: do you think discriminated unions can work with nested objects and arrays? Commit to yes or no.
Concept: Learn how to use discriminated unions inside nested structures and arrays for complex data modeling.
Example: interface Success { status: 'success'; data: { items: string[] }; } interface Failure { status: 'failure'; error: { message: string }; } type Response = Success | Failure; function handleResponse(res: Response) { if (res.status === 'success') { console.log(res.data.items.length); } else { console.error(res.error.message); } } TypeScript narrows deeply inside nested objects.
Result
You can model and safely access complex data with discriminated unions.
Discriminated unions are powerful enough for real-world complex data shapes, not just simple cases.
Under the Hood
TypeScript uses the discriminator property to narrow the union type at compile time. When you check the discriminator's value, TypeScript filters the union to only the matching type. This narrowing happens during static analysis, so no runtime cost is added. The compiler tracks the unique literal values of the discriminator to know which type matches.
Why designed this way?
Discriminated unions were designed to solve the problem of safely working with unions of object types without verbose manual type checks. The shared discriminator property provides a simple, consistent way for the compiler to distinguish types. Alternatives like manual type guards were error-prone and verbose. This design balances safety, simplicity, and developer ergonomics.
┌───────────────────────────────┐
│          Union Type           │
│  ┌───────────────┐            │
│  │ Type A        │            │
│  │ kind: 'a'     │            │
│  │ other props   │            │
│  └───────────────┘            │
│           │                   │
│           ▼                   │
│  ┌───────────────┐            │
│  │ Type B        │            │
│  │ kind: 'b'     │            │
│  │ other props   │            │
│  └───────────────┘            │
│           │                   │
│           ▼                   │
│  TypeScript narrows by 'kind' │
└───────────────────────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does TypeScript check the discriminator property at runtime automatically? Commit to yes or no.
Common Belief:TypeScript automatically checks the discriminator property at runtime to enforce types.
Tap to reveal reality
Reality:TypeScript only checks types at compile time. The discriminator property is a normal property at runtime with no automatic checks.
Why it matters:Relying on TypeScript for runtime checks can cause bugs if the data is malformed or comes from outside sources.
Quick: Can any property be used as a discriminator if it has unique values? Commit to yes or no.
Common Belief:Any property with unique values can serve as a discriminator for unions.
Tap to reveal reality
Reality:The discriminator must be a literal type property shared by all union members with unique values. Complex or optional properties don't work well.
Why it matters:Using the wrong property can cause TypeScript to fail to narrow types, leading to unsafe code.
Quick: Does adding more types to a discriminated union always require changing existing code? Commit to yes or no.
Common Belief:Adding new types to a discriminated union breaks existing code and requires many changes.
Tap to reveal reality
Reality:If you use exhaustiveness checking, TypeScript will warn you to update code, but otherwise existing code still works safely.
Why it matters:Knowing this helps manage code evolution and avoid unexpected bugs.
Quick: Can discriminated unions be used only with object types? Commit to yes or no.
Common Belief:Discriminated unions only work with object types.
Tap to reveal reality
Reality:Discriminated unions require object types with a common literal property; they don't apply to primitives or other types.
Why it matters:Trying to use discriminated unions with primitives leads to confusion and errors.
Expert Zone
1
Discriminated unions rely on literal types for the discriminator; using string enums or union of literals can improve maintainability.
2
Exhaustiveness checking with the never type is a powerful pattern to catch missing cases but requires discipline to maintain.
3
Discriminated unions can be combined with mapped types and conditional types for advanced type transformations and API design.
When NOT to use
Avoid discriminated unions when types do not share a common literal property or when runtime performance is critical and you want to avoid extra property checks. In such cases, consider using classes with instanceof checks or tagged classes.
Production Patterns
In real-world TypeScript projects, discriminated unions are widely used for modeling API responses, Redux action types, and complex configuration objects. They enable safe pattern matching and reduce runtime errors by leveraging compile-time checks.
Connections
Pattern matching
Discriminated unions enable pattern matching by letting code branch safely on type variants.
Understanding discriminated unions helps grasp how pattern matching works in functional languages, where types are matched by tags.
Algebraic Data Types (ADTs)
Discriminated unions are TypeScript's way to express sum types, a core part of ADTs.
Knowing discriminated unions connects you to a fundamental concept in type theory and functional programming.
Biology: Species classification
Just like discriminated unions classify data by a shared property, biology classifies organisms by shared traits to distinguish species.
Seeing classification in biology helps understand how discriminators separate types in programming.
Common Pitfalls
#1Forgetting to use a literal type for the discriminator
Wrong approach:interface Cat { kind: string; meow: () => void; } interface Dog { kind: string; bark: () => void; } type Pet = Cat | Dog;
Correct approach:interface Cat { kind: 'cat'; meow: () => void; } interface Dog { kind: 'dog'; bark: () => void; } type Pet = Cat | Dog;
Root cause:Using a general string type for 'kind' prevents TypeScript from distinguishing types, losing narrowing benefits.
#2Not handling all cases in a switch statement
Wrong approach:function speak(pet: Pet) { switch (pet.kind) { case 'cat': pet.meow(); break; // missing dog case } }
Correct approach:function speak(pet: Pet) { switch (pet.kind) { case 'cat': pet.meow(); break; case 'dog': pet.bark(); break; } }
Root cause:Ignoring some union members leads to runtime errors or missed behavior.
#3Assuming TypeScript checks discriminator at runtime
Wrong approach:const pet = { kind: 'cat', meow: () => console.log('meow') }; if (pet.kind === 'dog') { pet.bark(); // runtime error, no TypeScript check }
Correct approach:Use proper type guards or ensure data matches types before use to avoid runtime errors.
Root cause:TypeScript types are erased at runtime; runtime checks must be explicit.
Key Takeaways
Discriminated unions let TypeScript safely distinguish between multiple object types using a shared literal property called the discriminator.
They improve code safety and clarity by enabling automatic type narrowing based on the discriminator's value.
Using switch statements on the discriminator allows clean, exhaustive handling of all union members.
Exhaustiveness checking with the never type helps catch missing cases at compile time, preventing bugs.
Discriminated unions are powerful for modeling complex data and are widely used in real-world TypeScript projects.