Lesson 3.3: Union and Literal Types
Duration: 55 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Create union types that accept multiple type options
- Use literal types for exact value constraints
- Narrow types using type guards
- Combine unions and literals for precise type definitions
Union Types: Either This or That
A union type allows a variable to hold values of different types. Use the pipe symbol (|) to combine types:
let id: string | number;
id = 'user_123'; // OK - string
id = 456; // OK - number
id = true; // Error! Type 'boolean' is not assignable to type 'string | number'
Think of union types as an "or" relationship: the value can be string OR number.
Common Union Type Patterns
Nullable Values
The most common union is with null or undefined:
let username: string | null = null;
username = 'Alice'; // OK
username = null; // OK - explicitly no value
let age: number | undefined;
age = 25; // OK
age = undefined; // OK - not yet set
Function Parameters
Allow different input types:
function formatId(id: string | number): string {
return `ID: ${id}`;
}
formatId('abc123'); // OK
formatId(789); // OK
formatId(true); // Error!
Return Types
When a function might return different types:
function findUser(id: number): { name: string } | null {
if (id === 1) {
return { name: 'Alice' };
}
return null; // User not found
}
Working with Union Types
When you have a union type, TypeScript only allows operations that are valid for ALL types in the union:
function printId(id: string | number) {
// This works - both strings and numbers can be converted to strings
console.log(`ID: ${id}`);
// This fails - toUpperCase only exists on strings
console.log(id.toUpperCase()); // Error! Property 'toUpperCase' does not exist on type 'number'
}
To use type-specific methods, you need to narrow the type first.
Type Narrowing
Type narrowing is how you tell TypeScript which specific type you are working with. TypeScript tracks these checks and narrows the type automatically.
Using typeof
function printId(id: string | number) {
if (typeof id === 'string') {
// Inside this block, TypeScript knows id is a string
console.log(id.toUpperCase());
} else {
// Here TypeScript knows id is a number
console.log(id.toFixed(2));
}
}
Using Truthiness
function printName(name: string | null) {
if (name) {
// name is definitely a string here (not null)
console.log(`Hello, ${name.toUpperCase()}!`);
} else {
console.log('Hello, stranger!');
}
}
Using Equality Checks
function processValue(value: string | number | boolean) {
if (value === true) {
// value is true
console.log("It's true!");
} else if (value === false) {
// value is false
console.log("It's false!");
} else {
// value is string | number
console.log(`Value: ${value}`);
}
}
Literal Types: Exact Values
Literal types restrict a variable to specific values, not just types:
let direction: 'north' | 'south' | 'east' | 'west';
direction = 'north'; // OK
direction = 'south'; // OK
direction = 'up'; // Error! Type '"up"' is not assignable to type '"north" | "south" | "east" | "west"'
String Literals
type Status = 'pending' | 'approved' | 'rejected';
let orderStatus: Status = 'pending';
orderStatus = 'approved'; // OK
orderStatus = 'cancelled'; // Error! Not in the union
Number Literals
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll = 4; // OK
roll = 7; // Error! Type '7' is not assignable
Boolean Literal
Less common, but possible:
type AlwaysTrue = true;
let mustBeTrue: AlwaysTrue = true;
mustBeTrue = false; // Error!
Combining Unions and Literals
Literal types become powerful when combined with unions:
Example: HTTP Status Codes
type SuccessCode = 200 | 201 | 204;
type ErrorCode = 400 | 401 | 403 | 404 | 500;
type StatusCode = SuccessCode | ErrorCode;
function handleResponse(code: StatusCode) {
if (code === 200 || code === 201 || code === 204) {
console.log('Success!');
} else {
console.log('Error occurred');
}
}
Example: User Roles
type Role = 'admin' | 'editor' | 'viewer';
function checkPermission(role: Role, action: string): boolean {
if (role === 'admin') {
return true; // Admin can do anything
}
if (role === 'editor') {
return action !== 'delete';
}
return action === 'view';
}
checkPermission('admin', 'delete'); // OK
checkPermission('guest', 'view'); // Error! "guest" is not a valid Role
Example: Configuration Options
type Theme = 'light' | 'dark' | 'system';
type Language = 'en' | 'es' | 'fr' | 'de';
interface Settings {
theme: Theme;
language: Language;
notifications: boolean;
}
const userSettings: Settings = {
theme: 'dark',
language: 'en',
notifications: true,
};
Discriminated Unions
A powerful pattern combining literal types with objects. Each object in the union has a common property with a literal type that identifies it:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'square'; size: number };
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
// TypeScript knows shape has radius here
return Math.PI * shape.radius ** 2;
case 'rectangle':
// TypeScript knows shape has width and height here
return shape.width * shape.height;
case 'square':
// TypeScript knows shape has size here
return shape.size ** 2;
}
}
const circle: Shape = { kind: 'circle', radius: 5 };
const rectangle: Shape = { kind: 'rectangle', width: 10, height: 20 };
console.log(calculateArea(circle)); // 78.54...
console.log(calculateArea(rectangle)); // 200
The kind property is called the "discriminant" - it tells TypeScript which shape we are dealing with.
Practical Examples
Example 1: API Response Handler
type ApiResponse =
| { status: 'success'; data: string[] }
| { status: 'error'; message: string }
| { status: 'loading' };
function handleResponse(response: ApiResponse) {
switch (response.status) {
case 'success':
console.log('Data:', response.data.join(', '));
break;
case 'error':
console.log('Error:', response.message);
break;
case 'loading':
console.log('Loading...');
break;
}
}
handleResponse({ status: 'success', data: ['item1', 'item2'] });
handleResponse({ status: 'error', message: 'Not found' });
handleResponse({ status: 'loading' });
Example 2: Event System
type AppEvent =
| { type: 'click'; x: number; y: number }
| { type: 'keypress'; key: string }
| { type: 'scroll'; direction: 'up' | 'down' };
function logEvent(event: AppEvent) {
switch (event.type) {
case 'click':
console.log(`Clicked at (${event.x}, ${event.y})`);
break;
case 'keypress':
console.log(`Key pressed: ${event.key}`);
break;
case 'scroll':
console.log(`Scrolled ${event.direction}`);
break;
}
}
Example 3: Form Validation
type ValidationResult = { valid: true } | { valid: false; errors: string[] };
function validateEmail(email: string): ValidationResult {
const errors: string[] = [];
if (!email.includes('@')) {
errors.push('Email must contain @');
}
if (email.length < 5) {
errors.push('Email too short');
}
if (errors.length > 0) {
return { valid: false, errors };
}
return { valid: true };
}
const result = validateEmail('test');
if (result.valid) {
console.log('Email is valid!');
} else {
console.log('Errors:', result.errors.join(', '));
}
Exercises
Exercise 1: Create a Union Type
Create a type for a variable that can hold either a user ID (number) or a username (string), then write a function that accepts this type and returns a formatted string.
Solution
type UserIdentifier = number | string;
function formatIdentifier(id: UserIdentifier): string {
if (typeof id === 'number') {
return `User #${id}`;
}
return `@${id}`;
}
console.log(formatIdentifier(123)); // "User #123"
console.log(formatIdentifier('alice')); // "@alice"
Exercise 2: Literal Type for Days
Create a type for days of the week and a function that tells if a day is a weekend:
Solution
type DayOfWeek = 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday';
function isWeekend(day: DayOfWeek): boolean {
return day === 'Saturday' || day === 'Sunday';
}
console.log(isWeekend('Monday')); // false
console.log(isWeekend('Saturday')); // true
console.log(isWeekend('Someday')); // Error! Not a valid day
Exercise 3: Discriminated Union for Payments
Create a discriminated union for different payment methods:
Solution
type Payment =
| { method: 'credit_card'; cardNumber: string; cvv: string }
| { method: 'paypal'; email: string }
| { method: 'bank_transfer'; accountNumber: string; routingNumber: string };
function processPayment(payment: Payment): string {
switch (payment.method) {
case 'credit_card':
return `Processing card ending in ${payment.cardNumber.slice(-4)}`;
case 'paypal':
return `Processing PayPal payment for ${payment.email}`;
case 'bank_transfer':
return `Processing bank transfer to account ${payment.accountNumber}`;
}
}
console.log(
processPayment({
method: 'credit_card',
cardNumber: '1234567890123456',
cvv: '123',
})
);
// "Processing card ending in 3456"
Exercise 4: Nullable Search Result
Create a function that searches an array and returns the item or null:
Solution
type SearchResult = string | null;
function findItem(items: string[], query: string): SearchResult {
for (const item of items) {
if (item.toLowerCase().includes(query.toLowerCase())) {
return item;
}
}
return null;
}
const fruits = ['Apple', 'Banana', 'Cherry', 'Date'];
const result = findItem(fruits, 'ban');
if (result !== null) {
console.log(`Found: ${result}`);
} else {
console.log('Not found');
}
Key Takeaways
- Union types use
|: Combine types with the pipe symbol to allow multiple options - Literal types restrict values: Use exact strings or numbers as types
- Type narrowing is automatic: TypeScript tracks
typeof, truthiness, and equality checks - Discriminated unions are powerful: Add a common literal property to distinguish object variants
- Unions with null/undefined: The most common pattern for optional or nullable values
- Literal unions replace enums: Often cleaner than traditional enum declarations
Resources
| Resource | Type | Description |
|---|---|---|
| TypeScript Handbook: Union Types | Documentation | Official union types guide |
| TypeScript Handbook: Narrowing | Documentation | Deep dive into type narrowing |
| TypeScript Handbook: Literal Types | Documentation | Official literal types guide |
Next Lesson
Now that you can create flexible types with unions and precise types with literals, let us learn how to describe complex object structures using interfaces.