Discriminated union narrowing helps TypeScript know exactly which type you are working with by checking a special property. This makes your code safer and easier to understand.
Discriminated union narrowing in Typescript
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; size: number };
function area(shape: Shape) {
if (shape.kind === 'circle') {
// shape is narrowed to circle
} else if (shape.kind === 'square') {
// shape is narrowed to square
}
}The kind property is the discriminant that TypeScript uses to narrow the type.
Use simple string or number literals for the discriminant property for best results.
type is the discriminant property. Checking it narrows the type inside each branch.type Pet =
| { type: 'dog'; barkVolume: number }
| { type: 'cat'; livesLeft: number };
function describePet(pet: Pet) {
if (pet.type === 'dog') {
console.log(`Dog barks at volume ${pet.barkVolume}`);
} else {
console.log(`Cat has ${pet.livesLeft} lives left`);
}
}switch on the discriminant property status narrows the type in each case.type Response =
| { status: 'success'; data: string }
| { status: 'error'; message: string };
function handleResponse(res: Response) {
switch (res.status) {
case 'success':
console.log(`Data: ${res.data}`);
break;
case 'error':
console.log(`Error: ${res.message}`);
break;
}
}This program defines a Vehicle type with two shapes. The describeVehicle function uses the kind property to know which shape it is working with and prints details accordingly.
type Vehicle =
| { kind: 'car'; wheels: 4; brand: string }
| { kind: 'bike'; wheels: 2; hasBell: boolean };
function describeVehicle(vehicle: Vehicle) {
if (vehicle.kind === 'car') {
console.log(`Car brand: ${vehicle.brand}, wheels: ${vehicle.wheels}`);
} else if (vehicle.kind === 'bike') {
console.log(`Bike has bell: ${vehicle.hasBell}, wheels: ${vehicle.wheels}`);
}
}
const myCar: Vehicle = { kind: 'car', wheels: 4, brand: 'Toyota' };
const myBike: Vehicle = { kind: 'bike', wheels: 2, hasBell: true };
describeVehicle(myCar);
describeVehicle(myBike);Always use a common property with unique literal values for each type to enable discriminated union narrowing.
TypeScript automatically narrows the type inside the branches after checking the discriminant.
This technique helps avoid manual type assertions and makes your code safer.
Discriminated union narrowing uses a shared property to tell TypeScript which type you have.
Check the discriminant property to safely access type-specific properties.
This makes your code easier to read and less error-prone.
Practice
What is the main purpose of a discriminated union in TypeScript?
Solution
Step 1: Understand discriminated union concept
A discriminated union uses a shared property (called discriminant) to identify which type is currently in use.Step 2: Identify the purpose of the discriminant property
This shared property helps TypeScript narrow the type safely and access type-specific properties.Final Answer:
To use a common property to distinguish between different types in a union -> Option DQuick Check:
Discriminated union = common property for type narrowing [OK]
- Thinking discriminated unions require identical properties
- Believing unions can't be narrowed by properties
- Confusing discriminated unions with type intersections
Which of the following is the correct syntax to define a discriminated union type in TypeScript?
type Shape = ?
Solution
Step 1: Check for a shared discriminant property
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number } uses 'kind' as a string literal property to distinguish types, which is correct.Step 2: Verify property types and union structure
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number } correctly defines two object types with 'kind' discriminant and specific properties.Final Answer:
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number } -> Option CQuick Check:
Discriminated union needs a shared literal property [OK]
- Using & instead of | for union
- Not using a literal type for discriminant
- Using number instead of string literal for discriminant
What will be the output of the following TypeScript code?
type Animal =
| { type: 'dog'; barkVolume: number }
| { type: 'cat'; livesLeft: number };
function describe(animal: Animal) {
if (animal.type === 'dog') {
return `Dog barks at volume ${animal.barkVolume}`;
} else {
return `Cat has ${animal.livesLeft} lives left`;
}
}
console.log(describe({ type: 'cat', livesLeft: 7 }));Solution
Step 1: Analyze the input object and type check
The input has type 'cat' and livesLeft 7, matching the second union member.Step 2: Follow the if condition and return value
Since animal.type is 'cat', the else branch runs, returning "Cat has 7 lives left".Final Answer:
"Cat has 7 lives left" -> Option AQuick Check:
Discriminant 'type' guides correct property access [OK]
- Assuming wrong branch runs
- Expecting compile or runtime errors
- Confusing property names between types
Identify the error in this TypeScript code using discriminated unions:
type Vehicle =
| { kind: 'car'; wheels: number }
| { kind: 'bike'; pedals: number };
function getWheels(vehicle: Vehicle) {
if (vehicle.kind === 'car') {
return vehicle.wheels;
} else {
return vehicle.wheels;
}
}Solution
Step 1: Check property access in each branch
In the else branch, vehicle.kind is 'bike', but code accesses vehicle.wheels which does not exist on bike.Step 2: Understand TypeScript error
This causes a compile-time error because 'wheels' is not a property of the bike type.Final Answer:
Accessing 'wheels' on bike type causes error -> Option BQuick Check:
Access only properties valid for narrowed type [OK]
- Assuming all properties exist on all types
- Ignoring discriminant for narrowing
- Thinking missing return type causes error
Given the following discriminated union, how can you safely add a new shape type { kind: 'triangle'; base: number; height: number } and update the area function to handle it?
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
}
}Solution
Step 1: Extend the discriminated union type
Add { kind: 'triangle'; base: number; height: number } to the Shape union to include the new shape.Step 2: Update the area function switch
Add a new case 'triangle' that calculates area as 0.5 * base * height to handle the new shape safely.Final Answer:
Add triangle to Shape union and add a case 'triangle' in switch returning 0.5 * base * height -> Option AQuick Check:
Update union and switch for new discriminant case [OK]
- Forgetting to update the function for new type
- Using default case instead of explicit case
- Removing switch and losing type safety
Ready to prove it?
Pass the test and this pattern moves unknown
