Prerequisites
- Strong understanding of TypeScript basics ๐
- Familiarity with interfaces and types โก
- Knowledge of type aliases ๐ป
What you'll learn
- Create flexible types with unions ๐ฏ
- Combine types using intersections ๐๏ธ
- Use discriminated unions effectively ๐
- Build powerful type systems โจ
๐ฏ Introduction
Welcome to the colorful world of union and intersection types in TypeScript! ๐ In this guide, weโll explore how to combine types like a master chef combines ingredients to create something amazing.
Think of union types as โORโ operations (this OR that) ๐|๐ and intersection types as โANDโ operations (this AND that) ๐&๐. Together, theyโre your Swiss Army knife for creating flexible, precise type systems that can handle any scenario!
By the end of this tutorial, youโll be combining types like a TypeScript wizard, creating APIs that are both flexible and type-safe! Letโs dive in! ๐โโ๏ธ
๐ Understanding Union and Intersection Types
๐ค What Are Union Types?
Union types are like a restaurant menu with options ๐ฝ๏ธ - you can choose this OR that! They represent values that can be one of several types.
The syntax uses the pipe symbol |
:
type Meal = "pizza" | "burger" | "salad";
type PaymentMethod = "card" | "cash" | "crypto";
๐ก What Are Intersection Types?
Intersection types are like a combo meal ๐ฑ - you get this AND that! They combine multiple types into one.
The syntax uses the ampersand &
:
type Person = { name: string } & { age: number };
// Result: { name: string; age: number }
๐ฏ Why Use Them?
Hereโs why these types are game-changers:
- Flexible APIs ๐: Handle multiple input types gracefully
- Precise Modeling ๐ฏ: Represent real-world scenarios accurately
- Type Safety ๐ก๏ธ: Catch errors while allowing flexibility
- Better IntelliSense ๐ป: IDE understands all possibilities
- Composability ๐๏ธ: Build complex types from simple ones
Real-world example: A payment system ๐ณ that accepts credit cards, PayPal, or cryptocurrency - thatโs a union type in action!
๐ง Basic Syntax and Usage
๐ Union Types Basics
Letโs start with union types:
// ๐ Basic union types
type Status = "idle" | "loading" | "success" | "error";
type Size = "small" | "medium" | "large";
// ๐จ Union with different types
type StringOrNumber = string | number;
type BooleanOrNull = boolean | null;
// ๐ Using unions
let value: StringOrNumber = "hello";
value = 42; // Also valid!
// value = true; // Error! boolean not in union
// ๐ฏ Function with union parameters
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase();
} else {
return value.toFixed(2);
}
}
console.log(formatValue("hello")); // "HELLO"
console.log(formatValue(3.14159)); // "3.14"
// ๐ท๏ธ Union with objects
type SuccessResponse = {
status: "success";
data: any;
};
type ErrorResponse = {
status: "error";
message: string;
code: number;
};
type ApiResponse = SuccessResponse | ErrorResponse;
// ๐ฎ Using the union
function handleResponse(response: ApiResponse) {
if (response.status === "success") {
console.log("โ
Data:", response.data);
} else {
console.log("โ Error:", response.message);
}
}
๐จ Intersection Types Basics
Now letโs explore intersection types:
// ๐ค Basic intersection
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
// Result: { name: string; age: number }
const person: Person = {
name: "Alice",
age: 30
};
// ๐๏ธ Combining interfaces
interface Timestamp {
createdAt: Date;
updatedAt: Date;
}
interface Author {
authorId: string;
authorName: string;
}
type Article = {
title: string;
content: string;
} & Timestamp & Author;
const article: Article = {
title: "TypeScript Magic โจ",
content: "Union and intersection types are awesome!",
createdAt: new Date(),
updatedAt: new Date(),
authorId: "123",
authorName: "Alice"
};
// ๐ Function intersections
type Logger = {
log: (message: string) => void;
};
type Counter = {
count: number;
increment: () => void;
};
type LoggingCounter = Logger & Counter;
const loggingCounter: LoggingCounter = {
count: 0,
increment() {
this.count++;
this.log(`Count is now ${this.count}`);
},
log(message) {
console.log(`๐ ${message}`);
}
};
๐ฏ Combining Unions and Intersections
// ๐ Complex type combinations
type Admin = {
role: "admin";
permissions: string[];
};
type User = {
role: "user";
subscription: "free" | "premium";
};
type Guest = {
role: "guest";
sessionId: string;
};
// Union of different user types
type AnyUser = Admin | User | Guest;
// Add common properties with intersection
type AuthenticatedUser = AnyUser & {
lastLogin: Date;
isActive: boolean;
};
// ๐ฎ Using discriminated unions
function getWelcomeMessage(user: AnyUser): string {
switch (user.role) {
case "admin":
return `๐ Welcome Admin! You have ${user.permissions.length} permissions`;
case "user":
return `๐ Welcome! You have a ${user.subscription} account`;
case "guest":
return `๐ Welcome Guest! Session: ${user.sessionId}`;
}
}
๐ก Practical Examples
๐ Example 1: E-commerce System
Letโs build a flexible payment system using unions:
// ๐ณ Payment method types
type CreditCard = {
type: "credit-card";
cardNumber: string;
cvv: string;
expiryDate: string;
emoji: "๐ณ";
};
type PayPal = {
type: "paypal";
email: string;
password: string;
emoji: "๐ต";
};
type Cryptocurrency = {
type: "crypto";
walletAddress: string;
currency: "BTC" | "ETH" | "USDT";
emoji: "๐ช";
};
type BankTransfer = {
type: "bank-transfer";
accountNumber: string;
routingNumber: string;
emoji: "๐ฆ";
};
// Union of all payment methods
type PaymentMethod = CreditCard | PayPal | Cryptocurrency | BankTransfer;
// ๐๏ธ Order with payment
interface Order {
id: string;
items: Array<{ name: string; price: number; quantity: number }>;
total: number;
}
type PaidOrder = Order & {
paymentMethod: PaymentMethod;
paidAt: Date;
transactionId: string;
};
// ๐ฏ Process payment based on type
class PaymentProcessor {
processPayment(order: Order, payment: PaymentMethod): PaidOrder | null {
console.log(`\n${payment.emoji} Processing ${payment.type} payment...`);
switch (payment.type) {
case "credit-card":
console.log(`๐ณ Charging card ending in ${payment.cardNumber.slice(-4)}`);
break;
case "paypal":
console.log(`๐ต PayPal account: ${payment.email}`);
break;
case "crypto":
console.log(`๐ช Sending ${payment.currency} request to ${payment.walletAddress.slice(0, 10)}...`);
break;
case "bank-transfer":
console.log(`๐ฆ Initiating bank transfer from account ${payment.accountNumber}`);
break;
}
// Simulate payment processing
const success = Math.random() > 0.1; // 90% success rate
if (success) {
console.log("โ
Payment successful!");
return {
...order,
paymentMethod: payment,
paidAt: new Date(),
transactionId: `TXN-${Date.now()}`
};
} else {
console.log("โ Payment failed!");
return null;
}
}
// ๐ Get payment summary
getPaymentSummary(payment: PaymentMethod): string {
switch (payment.type) {
case "credit-card":
return `Card ****${payment.cardNumber.slice(-4)}`;
case "paypal":
return `PayPal (${payment.email})`;
case "crypto":
return `${payment.currency} (${payment.walletAddress.slice(0, 6)}...)`;
case "bank-transfer":
return `Bank Transfer (****${payment.accountNumber.slice(-4)})`;
}
}
}
// ๐ฎ Usage
const processor = new PaymentProcessor();
const order: Order = {
id: "ORD-123",
items: [
{ name: "TypeScript Book ๐", price: 39.99, quantity: 1 },
{ name: "Coffee โ", price: 4.99, quantity: 3 }
],
total: 54.96
};
// Try different payment methods
const creditCardPayment: CreditCard = {
type: "credit-card",
cardNumber: "1234567812345678",
cvv: "123",
expiryDate: "12/25",
emoji: "๐ณ"
};
const cryptoPayment: Cryptocurrency = {
type: "crypto",
walletAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f6E123",
currency: "ETH",
emoji: "๐ช"
};
processor.processPayment(order, creditCardPayment);
processor.processPayment(order, cryptoPayment);
๐จ Example 2: Form Validation System
Letโs create a flexible validation system:
// ๐ Validation rule types
type RequiredRule = {
type: "required";
message: string;
};
type MinLengthRule = {
type: "minLength";
value: number;
message: string;
};
type MaxLengthRule = {
type: "maxLength";
value: number;
message: string;
};
type PatternRule = {
type: "pattern";
regex: RegExp;
message: string;
};
type EmailRule = {
type: "email";
message: string;
};
// Union of all validation rules
type ValidationRule = RequiredRule | MinLengthRule | MaxLengthRule | PatternRule | EmailRule;
// ๐ท๏ธ Field with validation
type FormField<T = string> = {
name: string;
label: string;
value: T;
rules: ValidationRule[];
errors: string[];
touched: boolean;
};
// ๐๏ธ Form state
type FormState = {
fields: Record<string, FormField>;
isValid: boolean;
isSubmitting: boolean;
};
// ๐ฏ Validation system
class FormValidator {
validateField(field: FormField): string[] {
const errors: string[] = [];
for (const rule of field.rules) {
const error = this.validateRule(field.value, rule);
if (error) {
errors.push(error);
}
}
return errors;
}
private validateRule(value: string, rule: ValidationRule): string | null {
switch (rule.type) {
case "required":
return value.trim() === "" ? rule.message : null;
case "minLength":
return value.length < rule.value ? rule.message : null;
case "maxLength":
return value.length > rule.value ? rule.message : null;
case "pattern":
return !rule.regex.test(value) ? rule.message : null;
case "email":
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return !emailRegex.test(value) ? rule.message : null;
}
}
// ๐ Validate entire form
validateForm(form: FormState): FormState {
const updatedFields: Record<string, FormField> = {};
let isValid = true;
for (const [name, field] of Object.entries(form.fields)) {
const errors = this.validateField(field);
updatedFields[name] = { ...field, errors };
if (errors.length > 0) {
isValid = false;
}
}
return {
...form,
fields: updatedFields,
isValid
};
}
}
// ๐ฎ Example usage
const validator = new FormValidator();
const loginForm: FormState = {
fields: {
email: {
name: "email",
label: "Email Address ๐ง",
value: "user@example",
rules: [
{ type: "required", message: "Email is required" },
{ type: "email", message: "Please enter a valid email" }
],
errors: [],
touched: true
},
password: {
name: "password",
label: "Password ๐",
value: "123",
rules: [
{ type: "required", message: "Password is required" },
{ type: "minLength", value: 8, message: "Password must be at least 8 characters" },
{ type: "pattern", regex: /(?=.*[A-Z])(?=.*[0-9])/, message: "Password must contain uppercase and numbers" }
],
errors: [],
touched: true
}
},
isValid: false,
isSubmitting: false
};
const validatedForm = validator.validateForm(loginForm);
// Display results
for (const [name, field] of Object.entries(validatedForm.fields)) {
console.log(`\n${field.label}:`);
console.log(` Value: "${field.value}"`);
if (field.errors.length > 0) {
console.log(` โ Errors:`);
field.errors.forEach(error => console.log(` - ${error}`));
} else {
console.log(` โ
Valid!`);
}
}
console.log(`\n๐ท๏ธ Form is ${validatedForm.isValid ? 'valid โ
' : 'invalid โ'}`);
๐ฎ Example 3: Game State Management
// ๐ฎ Game entities with intersections
type Position = { x: number; y: number };
type Velocity = { vx: number; vy: number };
type Health = { health: number; maxHealth: number };
type Sprite = { sprite: string; color: string };
// Base entity
type Entity = Position & Sprite & {
id: string;
type: string;
};
// Different entity types
type Player = Entity & Health & Velocity & {
type: "player";
score: number;
lives: number;
powerups: string[];
};
type Enemy = Entity & Health & Velocity & {
type: "enemy";
damage: number;
behavior: "aggressive" | "defensive" | "patrol";
};
type PowerUp = Entity & {
type: "powerup";
effect: "speed" | "shield" | "multishot";
duration: number;
};
type Obstacle = Entity & {
type: "obstacle";
solid: boolean;
destructible: boolean;
};
// Union of all game objects
type GameObject = Player | Enemy | PowerUp | Obstacle;
// ๐ฎ Game engine
class GameEngine {
private entities: Map<string, GameObject> = new Map();
// ๐ฏ Spawn entities
spawn<T extends GameObject>(entity: T): void {
this.entities.set(entity.id, entity);
console.log(`โจ Spawned ${entity.type} at (${entity.x}, ${entity.y})`);
}
// ๐ Update game state
update(): void {
for (const entity of this.entities.values()) {
this.updateEntity(entity);
}
}
private updateEntity(entity: GameObject): void {
switch (entity.type) {
case "player":
this.updatePlayer(entity);
break;
case "enemy":
this.updateEnemy(entity);
break;
case "powerup":
this.updatePowerUp(entity);
break;
case "obstacle":
// Obstacles don't update
break;
}
}
private updatePlayer(player: Player): void {
player.x += player.vx;
player.y += player.vy;
console.log(`๐ฎ Player at (${player.x}, ${player.y}) - Health: ${player.health}/${player.maxHealth}`);
}
private updateEnemy(enemy: Enemy): void {
// AI behavior based on type
switch (enemy.behavior) {
case "aggressive":
console.log(`๐ฟ Aggressive enemy chasing player!`);
break;
case "defensive":
console.log(`๐ก๏ธ Defensive enemy holding position`);
break;
case "patrol":
console.log(`๐ Patrolling enemy moving on route`);
break;
}
}
private updatePowerUp(powerup: PowerUp): void {
console.log(`โจ ${powerup.effect} powerup available!`);
}
// ๐ฅ Check collisions
checkCollision(a: Entity, b: Entity): boolean {
const distance = Math.sqrt(
Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2)
);
return distance < 10; // Simple radius check
}
}
// ๐ฎ Create game
const game = new GameEngine();
// Spawn entities
game.spawn<Player>({
id: "player-1",
type: "player",
x: 50,
y: 50,
vx: 0,
vy: 0,
health: 100,
maxHealth: 100,
score: 0,
lives: 3,
powerups: [],
sprite: "๐งโ๐",
color: "blue"
});
game.spawn<Enemy>({
id: "enemy-1",
type: "enemy",
x: 200,
y: 100,
vx: -1,
vy: 0,
health: 50,
maxHealth: 50,
damage: 10,
behavior: "aggressive",
sprite: "๐พ",
color: "red"
});
game.spawn<PowerUp>({
id: "powerup-1",
type: "powerup",
x: 150,
y: 150,
effect: "shield",
duration: 5000,
sprite: "๐ก๏ธ",
color: "gold"
});
// Run game loop
game.update();
๐ Advanced Concepts
๐งโโ๏ธ Discriminated Unions
The most powerful pattern with unions:
// ๐ฏ Discriminated union for state management
type LoadingState = {
status: "loading";
message: string;
};
type SuccessState<T> = {
status: "success";
data: T;
timestamp: Date;
};
type ErrorState = {
status: "error";
error: Error;
code: number;
retry: () => void;
};
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
// ๐จ Type-safe state handler
function handleAsyncState<T>(state: AsyncState<T>): void {
switch (state.status) {
case "loading":
console.log(`โณ ${state.message}`);
break;
case "success":
console.log(`โ
Success at ${state.timestamp.toLocaleTimeString()}`);
console.log(`๐ฆ Data:`, state.data);
break;
case "error":
console.log(`โ Error ${state.code}: ${state.error.message}`);
console.log(`๐ Retrying...`);
state.retry();
break;
}
}
// ๐ Advanced: Exhaustive checking
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function processState<T>(state: AsyncState<T>): string {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return "Success!";
case "error":
return "Error!";
default:
// TypeScript ensures all cases are handled
return assertNever(state);
}
}
๐๏ธ Advanced Intersection Patterns
// ๐งฌ Mixin pattern with intersections
type Constructor<T = {}> = new (...args: any[]) => T;
// Mixins
function Timestamped<T extends Constructor>(Base: T) {
return class extends Base {
createdAt = new Date();
updatedAt = new Date();
touch() {
this.updatedAt = new Date();
}
};
}
function Taggable<T extends Constructor>(Base: T) {
return class extends Base {
tags: string[] = [];
addTag(tag: string) {
this.tags.push(tag);
console.log(`๐ท๏ธ Added tag: ${tag}`);
}
};
}
// Base class
class Article {
constructor(public title: string, public content: string) {}
}
// Apply mixins
const TimestampedTaggableArticle = Taggable(Timestamped(Article));
// Use the mixed class
const article = new TimestampedTaggableArticle(
"TypeScript Mixins",
"Intersection types enable powerful patterns!"
);
article.addTag("typescript");
article.addTag("advanced");
article.touch();
console.log(`๐ Article: ${article.title}`);
console.log(`๐ท๏ธ Tags: ${article.tags.join(", ")}`);
console.log(`๐
Updated: ${article.updatedAt.toLocaleString()}`);
๐จ Type Guards with Unions
// ๐ Custom type guards
type Fish = {
type: "fish";
swim: () => void;
fins: number;
};
type Bird = {
type: "bird";
fly: () => void;
wings: number;
};
type Dog = {
type: "dog";
bark: () => void;
breed: string;
};
type Animal = Fish | Bird | Dog;
// Type guard functions
function isFish(animal: Animal): animal is Fish {
return animal.type === "fish";
}
function isBird(animal: Animal): animal is Bird {
return animal.type === "bird";
}
function isDog(animal: Animal): animal is Dog {
return animal.type === "dog";
}
// ๐ฏ Use type guards
function interactWithAnimal(animal: Animal): void {
console.log(`\n๐พ Interacting with ${animal.type}`);
if (isFish(animal)) {
console.log(`๐ Fish has ${animal.fins} fins`);
animal.swim();
} else if (isBird(animal)) {
console.log(`๐ฆ
Bird has ${animal.wings} wings`);
animal.fly();
} else if (isDog(animal)) {
console.log(`๐ Dog breed: ${animal.breed}`);
animal.bark();
}
}
// ๐ฎ Create animals
const animals: Animal[] = [
{
type: "fish",
fins: 3,
swim: () => console.log("๐ Swimming!")
},
{
type: "bird",
wings: 2,
fly: () => console.log("๐ฆ
Flying high!")
},
{
type: "dog",
breed: "Golden Retriever",
bark: () => console.log("๐ถ Woof woof!")
}
];
animals.forEach(interactWithAnimal);
๐ Conditional Types with Unions
// ๐งฌ Extract and exclude utilities
type Status = "idle" | "loading" | "success" | "error";
// Extract specific types
type LoadingStates = Extract<Status, "idle" | "loading">; // "idle" | "loading"
type CompleteStates = Extract<Status, "success" | "error">; // "success" | "error"
// Exclude types
type NonErrorStates = Exclude<Status, "error">; // "idle" | "loading" | "success"
type ActiveStates = Exclude<Status, "idle">; // "loading" | "success" | "error"
// ๐ฏ Distributive conditional types
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
type Test3 = IsString<string | number>; // boolean (true | false)
// ๐จ Filter union members
type FilterStrings<T> = T extends string ? T : never;
type StringMembers = FilterStrings<string | number | boolean | "hello" | "world">;
// Result: string | "hello" | "world"
// ๐บ๏ธ Map over union types
type EventTypes = "click" | "focus" | "blur";
type EventHandlers = {
[K in EventTypes as `on${Capitalize<K>}`]: (event: Event) => void;
};
// Result: { onClick: ..., onFocus: ..., onBlur: ... }
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Union Type Property Access
// โ Wrong - can't access properties not in all union members
type Cat = { type: "cat"; meow: () => void };
type Dog = { type: "dog"; bark: () => void };
type Pet = Cat | Dog;
function petSound(pet: Pet) {
// pet.bark(); // Error! 'bark' doesn't exist on Cat
// pet.meow(); // Error! 'meow' doesn't exist on Dog
}
// โ
Correct - use type guards
function petSoundGood(pet: Pet) {
if (pet.type === "dog") {
pet.bark(); // TypeScript knows it's a Dog
} else {
pet.meow(); // TypeScript knows it's a Cat
}
}
๐คฏ Pitfall 2: Intersection Type Conflicts
// โ Problem - conflicting property types
type A = { value: string };
type B = { value: number };
// type C = A & B; // value is 'never' - can't be string AND number!
// โ
Solution 1 - use union for the property
type A2 = { value: string | number };
type B2 = { value: number };
type C2 = A2 & B2; // value is number (more specific)
// โ
Solution 2 - different property names
type A3 = { textValue: string };
type B3 = { numValue: number };
type C3 = A3 & B3; // Both properties exist
๐ต Pitfall 3: Excessive Union Types
// โ Too many union members - hard to handle
type Status = "idle" | "loading" | "loaded" | "reloading" |
"error" | "retrying" | "success" | "partial" |
"cancelled" | "timeout" | "pending";
// โ
Better - group related states
type LoadState = "idle" | "loading" | "loaded";
type ErrorState = "error" | "timeout";
type SuccessState = "success" | "partial";
type BetterStatus = {
load: LoadState;
result?: ErrorState | SuccessState;
isRetrying?: boolean;
};
๐ค Pitfall 4: Wrong Type Guard Assumptions
// โ Unsafe type guard
function isString(value: unknown): value is string {
return typeof value === "object"; // Wrong check!
}
// โ
Correct type guard
function isStringCorrect(value: unknown): value is string {
return typeof value === "string";
}
// ๐ฏ Multiple checks for complex types
interface User {
name: string;
age: number;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
"age" in value &&
typeof (value as User).name === "string" &&
typeof (value as User).age === "number"
);
}
๐ฌ Pitfall 5: Forgetting Distributivity
// โ ๏ธ Conditional types distribute over unions
type Wrap<T> = T extends string ? [T] : T;
type Test1 = Wrap<string | number>;
// Result: [string] | number (NOT [string | number])
// ๐ง To prevent distribution, wrap in a tuple
type WrapNonDistributive<T> = [T] extends [string] ? [T] : T;
type Test2 = WrapNonDistributive<string | number>;
// Result: string | number (no wrapping)
// ๐ฏ When you want distribution
type StringifyAll<T> = T extends any ? `${T & string}` : never;
type Stringified = StringifyAll<"a" | "b" | "c">; // "a" | "b" | "c"
๐ ๏ธ Best Practices
1. ๐ฏ Use Discriminated Unions
// โ
Good - easy to distinguish
type Result<T> =
| { success: true; data: T }
| { success: false; error: Error };
function handleResult<T>(result: Result<T>) {
if (result.success) {
console.log("Data:", result.data);
} else {
console.log("Error:", result.error);
}
}
2. ๐ Name Union Types Clearly
// โ
Good - descriptive names
type PaymentStatus = "pending" | "processing" | "completed" | "failed";
type UserRole = "admin" | "moderator" | "user" | "guest";
// โ Avoid - generic names
type Status = "a" | "b" | "c";
type Type = 1 | 2 | 3;
3. ๐ก๏ธ Prefer Unions Over Enums
// โ
Union types - simpler and more flexible
type Direction = "north" | "south" | "east" | "west";
// Use with confidence
const move = (direction: Direction) => {
console.log(`Moving ${direction}`);
};
4. ๐จ Keep Intersections Simple
// โ
Good - clear composition
type Timestamps = { createdAt: Date; updatedAt: Date };
type Author = { authorId: string; authorName: string };
type Post = { title: string; content: string } & Timestamps & Author;
// โ Avoid - too many intersections
type Monster = A & B & C & D & E & F; // Hard to understand
5. โจ Use Type Guards Liberally
// โ
Create reusable type guards
const isError = <T>(result: T | Error): result is Error => {
return result instanceof Error;
};
const hasProperty = <T, K extends string>(
obj: T,
key: K
): obj is T & Record<K, unknown> => {
return obj != null && key in obj;
};
// Use throughout your code
if (isError(result)) {
console.error(result.message);
} else {
processData(result);
}
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Notification System
Create a flexible notification system using unions and intersections:
๐ Requirements:
- โ Multiple notification types (email, SMS, push, in-app)
- ๐ท๏ธ Priority levels and scheduling
- ๐ค User preferences and filtering
- ๐ Delivery tracking and retries
- ๐จ Rich content support
๐ Bonus Points:
- Add notification templates
- Implement batching
- Create analytics
๐ก Solution
๐ Click to see solution
// ๐ฏ Flexible notification system with unions and intersections
// Base notification properties
type NotificationBase = {
id: string;
userId: string;
priority: "low" | "normal" | "high" | "urgent";
scheduledFor?: Date;
expiresAt?: Date;
metadata?: Record<string, any>;
};
// Delivery tracking
type DeliveryInfo = {
attempts: number;
lastAttempt?: Date;
deliveredAt?: Date;
error?: string;
};
// Different notification channels
type EmailNotification = NotificationBase & {
type: "email";
to: string;
subject: string;
body: string;
attachments?: Array<{ name: string; url: string }>;
replyTo?: string;
};
type SMSNotification = NotificationBase & {
type: "sms";
phoneNumber: string;
message: string;
maxLength?: number;
};
type PushNotification = NotificationBase & {
type: "push";
title: string;
body: string;
icon?: string;
badge?: number;
data?: Record<string, any>;
actions?: Array<{ id: string; title: string; icon?: string }>;
};
type InAppNotification = NotificationBase & {
type: "in-app";
title: string;
message: string;
actionUrl?: string;
imageUrl?: string;
persistent: boolean;
};
// Union of all notification types
type Notification = EmailNotification | SMSNotification | PushNotification | InAppNotification;
// Notification with delivery status
type TrackedNotification = Notification & DeliveryInfo;
// User preferences
type UserPreferences = {
channels: Array<Notification["type"]>;
quiet_hours?: { start: string; end: string };
blockedCategories: string[];
maxPerDay?: number;
};
// ๐ Notification system
class NotificationSystem {
private notifications: Map<string, TrackedNotification> = new Map();
private userPreferences: Map<string, UserPreferences> = new Map();
// ๐จ Send notification
async send(notification: Notification): Promise<boolean> {
console.log(`\n๐จ Sending ${notification.type} notification...`);
// Check user preferences
const prefs = this.userPreferences.get(notification.userId);
if (!this.canSend(notification, prefs)) {
console.log("๐ซ Blocked by user preferences");
return false;
}
// Add delivery tracking
const tracked: TrackedNotification = {
...notification,
attempts: 0,
lastAttempt: new Date()
};
this.notifications.set(notification.id, tracked);
// Process based on type
return this.processNotification(tracked);
}
// ๐ฏ Process different notification types
private async processNotification(notification: TrackedNotification): Promise<boolean> {
notification.attempts++;
try {
switch (notification.type) {
case "email":
console.log(`๐ง Sending email to ${notification.to}`);
console.log(` Subject: ${notification.subject}`);
if (notification.attachments?.length) {
console.log(` ๐ ${notification.attachments.length} attachments`);
}
break;
case "sms":
console.log(`๐ฑ Sending SMS to ${notification.phoneNumber}`);
console.log(` Message (${notification.message.length} chars): ${notification.message}`);
break;
case "push":
console.log(`๐ Sending push notification`);
console.log(` Title: ${notification.title}`);
if (notification.actions?.length) {
console.log(` ๐ฏ ${notification.actions.length} actions available`);
}
break;
case "in-app":
console.log(`๐ฌ Showing in-app notification`);
console.log(` Title: ${notification.title}`);
console.log(` Persistent: ${notification.persistent ? "Yes" : "No"}`);
break;
}
// Simulate delivery
const success = Math.random() > 0.1;
if (success) {
notification.deliveredAt = new Date();
console.log("โ
Delivered successfully!");
return true;
} else {
throw new Error("Delivery failed");
}
} catch (error) {
notification.error = (error as Error).message;
console.log(`โ Failed: ${notification.error}`);
// Retry logic
if (notification.attempts < 3) {
console.log(`๐ Will retry (attempt ${notification.attempts}/3)`);
setTimeout(() => this.processNotification(notification), 1000 * notification.attempts);
}
return false;
}
}
// ๐ Check user preferences
private canSend(notification: Notification, prefs?: UserPreferences): boolean {
if (!prefs) return true;
// Check channel preference
if (!prefs.channels.includes(notification.type)) {
return false;
}
// Check quiet hours
if (prefs.quiet_hours) {
const now = new Date();
const hour = now.getHours();
const start = parseInt(prefs.quiet_hours.start);
const end = parseInt(prefs.quiet_hours.end);
if (hour >= start && hour < end) {
console.log("๐ In quiet hours");
return notification.priority === "urgent";
}
}
return true;
}
// ๐ค Set user preferences
setUserPreferences(userId: string, prefs: UserPreferences): void {
this.userPreferences.set(userId, prefs);
console.log(`๐ง Updated preferences for user ${userId}`);
}
// ๐ Get statistics
getStats(): void {
const stats = {
total: this.notifications.size,
delivered: 0,
failed: 0,
pending: 0,
byType: {} as Record<string, number>
};
for (const notification of this.notifications.values()) {
if (notification.deliveredAt) stats.delivered++;
else if (notification.error && notification.attempts >= 3) stats.failed++;
else stats.pending++;
stats.byType[notification.type] = (stats.byType[notification.type] || 0) + 1;
}
console.log("\n๐ Notification Statistics:");
console.log(`๐จ Total: ${stats.total}`);
console.log(`โ
Delivered: ${stats.delivered}`);
console.log(`โ Failed: ${stats.failed}`);
console.log(`โณ Pending: ${stats.pending}`);
console.log(`๐ท๏ธ By Type:`, stats.byType);
}
}
// ๐ฎ Test the system
const notificationSystem = new NotificationSystem();
// Set user preferences
notificationSystem.setUserPreferences("user-123", {
channels: ["email", "push", "in-app"],
quiet_hours: { start: "22", end: "8" },
blockedCategories: ["marketing"],
maxPerDay: 10
});
// Send various notifications
notificationSystem.send({
id: "notif-1",
userId: "user-123",
type: "email",
to: "[email protected]",
subject: "Welcome to TypeScript! ๐",
body: "Thanks for joining our TypeScript course!",
priority: "normal",
attachments: [
{ name: "guide.pdf", url: "https://example.com/guide.pdf" }
]
});
notificationSystem.send({
id: "notif-2",
userId: "user-123",
type: "push",
title: "New Achievement! ๐",
body: "You've completed 10 lessons",
badge: 1,
priority: "normal",
actions: [
{ id: "view", title: "View Achievement", icon: "๐" },
{ id: "share", title: "Share", icon: "๐ค" }
]
});
notificationSystem.send({
id: "notif-3",
userId: "user-123",
type: "sms",
phoneNumber: "+1234567890",
message: "Your verification code is 123456",
priority: "urgent"
});
notificationSystem.send({
id: "notif-4",
userId: "user-123",
type: "in-app",
title: "Daily Tip ๐ก",
message: "Did you know? TypeScript has amazing union types!",
actionUrl: "/tips/unions",
persistent: false,
priority: "low"
});
// Show statistics
setTimeout(() => {
notificationSystem.getStats();
}, 2000);
๐ Key Takeaways
Youโve mastered union and intersection types! Hereโs what you can now do:
- โ Create flexible types with unions (this OR that) ๐ช
- โ Combine types with intersections (this AND that) ๐ก๏ธ
- โ Use discriminated unions for type-safe state ๐ฏ
- โ Build complex type systems from simple parts ๐
- โ Write APIs that are both flexible and safe! ๐
Remember: Union types give flexibility, intersection types give power. Together, theyโre unstoppable! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered union and intersection types!
Hereโs what to do next:
- ๐ป Complete the notification system exercise
- ๐๏ธ Refactor existing code to use discriminated unions
- ๐ Move on to our next tutorial: Type Assertions and Type Guards
- ๐ Build a state machine using union types!
Remember: These patterns are everywhere in real TypeScript code. The more you practice, the more natural they become! ๐
Happy coding! ๐๐โจ