Module lessons (2/2)
Unions and Narrowing
In the real world, variables and API responses do not always have a single fixed type. TypeScript offers Union Types to handle variability, and Type Narrowing to operate safely on them at runtime.
Union Types
A union type allows a variable to accept values of different types. It is expressed using the vertical bar (|) symbol:
let result: number | string;
result = 42; // Valid
result = 'Error 404'; // ValidHowever, when working with a union type, we cannot directly call methods that belong to only one of the types (for example, we cannot call .toUpperCase() if the variable can also be a number). We must first "narrow" the type.
Type Narrowing
Type Narrowing is the process where TypeScript analyzes control flow structures (such as if or switch) to deduce a more specific type for a variable at runtime.
There are several ways to perform narrowing:
1. The typeof Operator
Ideal for distinguishing primitive types:
function printLength(value: string | number) {
if (typeof value === 'string') {
// TypeScript knows 'value' is a string here
console.log(value.length);
} else {
// TypeScript knows 'value' is a number here
console.log(value.toFixed(2));
}
}2. The in Operator
Used to check if a specific property exists on an object:
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
function move(animal: Fish | Bird) {
if ('swim' in animal) {
animal.swim(); // Narrowing to Fish
} else {
animal.fly(); // Narrowing to Bird
}
}Discriminated Unions
The Discriminated Unions pattern consists of creating objects that share a common property with a unique literal value (called a discriminator). TypeScript recognizes this discriminator and automatically narrows the type within conditional blocks.
interface SuccessResponse {
status: 'success'; // Literal discriminator
data: string;
}
interface ErrorResponse {
status: 'error'; // Literal discriminator
errorMessage: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse) {
if (response.status === 'success') {
console.log('Data received:', response.data);
} else {
console.error('An error occurred:', response.errorMessage);
}
}Try it yourself
Exercise 1: Union Types
Declare a variable named id that can be either a number or a string. Initialize it first with the number 101, then assign the string value 'USER-101'.
Show hint
Use the | operator to join number and string in the let variable declaration.
Solution available after 3 attempts
Exercise 2: Basic Type Narrowing
Create a function named formatInput that accepts a parameter input of type string or number. If input is a string, return input converted to uppercase. If it is a number, return input multiplied by 2. Specify types explicitly.
Show hint
Use typeof input === 'string' inside an if block to branch the behavior.
Solution available after 3 attempts
Exercise 3: Narrowing with 'in'
Given the two interfaces Car (with method drive) and Boat (with method sail), write a function named moveVehicle that accepts a parameter vehicle of type Car or Boat. If vehicle has the drive property, execute the drive() method. Otherwise, execute the sail() method.
Show hint
Use the in operator in the form 'drive' in vehicle to narrow the interface.
Solution available after 3 attempts
Exercise 4: Discriminated Union Shape
Define a type Shape which is the union of two types: Circle and Square. Circle has a kind property set to 'circle' (literal value) and a radius (number). Square has a kind property set to 'square' (literal value) and a side (number). Then write a function getArea that accepts shape of type Shape and returns the area as a number (for circle Math.PI * radius * radius, for square side * side).
Show hint
Use shape.kind === 'circle' inside getArea to discriminate the type and calculate the correct area.
Solution available after 3 attempts