Advanced TypeScript Patterns: Deep Dive into Complex Real-World Types
TypeScript’s true power shines when dealing with complex real-world scenarios. Let’s explore some advanced patterns using practical examples.
Handling Complex E-commerce Order Systems
Let’s start with a real-world example of modeling an e-commerce order system:
interface BaseEntity {
id: string;
createdAt: Date;
updatedAt: Date;
}
type ProductVariantAttribute = {
name: string;
value: string;
};
interface ProductVariant extends BaseEntity {
sku: string;
price: number;
attributes: ProductVariantAttribute[];
inventory: {
available: number;
reserved: number;
backorder: boolean;
};
}
type OrderStatus =
| "pending"
| "processing"
| "shipped"
| "delivered"
| "cancelled";
type PaymentMethod = "credit_card" | "paypal" | "crypto";
interface PaymentDetails {
method: PaymentMethod;
status: "pending" | "completed" | "failed" | "refunded";
transactionId: string;
amount: number;
metadata: Record<string, unknown>;
}
interface Address {
street: string;
city: string;
state: string;
country: string;
postalCode: string;
}
interface ShippingDetails {
address: Address;
carrier: string;
trackingNumber?: string;
estimatedDelivery?: Date;
shippingMethod: "standard" | "express" | "overnight";
}
interface Order extends BaseEntity {
orderNumber: string;
customer: {
id: string;
email: string;
name: string;
};
items: Array<{
variant: ProductVariant;
quantity: number;
price: number; // Price at time of purchase
discounts?: Array<{
code: string;
amount: number;
type: "percentage" | "fixed";
}>;
}>;
status: OrderStatus;
payment: PaymentDetails;
shipping: ShippingDetails;
totals: {
subtotal: number;
tax: number;
shipping: number;
discounts: number;
total: number;
};
metadata: Record<string, unknown>;
}
Type-Safe Event System
Here’s how we can create a type-safe event system for our order processing:
interface OrderEvents {
"order.created": {
order: Order;
timestamp: Date;
};
"order.updated": {
order: Order;
changes: Partial<Order>;
timestamp: Date;
};
"order.payment_failed": {
order: Order;
payment: PaymentDetails;
error: {
code: string;
message: string;
};
};
"order.shipped": {
order: Order;
shipping: ShippingDetails;
notificationSent: boolean;
};
}
class TypedEventEmitter<Events extends Record<string, any>> {
private listeners: Partial<Record<keyof Events, Function[]>> = {};
on<E extends keyof Events>(
event: E,
listener: (payload: Events[E]) => void
): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]?.push(listener);
}
emit<E extends keyof Events>(event: E, payload: Events[E]): void {
this.listeners[event]?.forEach((listener) => listener(payload));
}
}
Utility Types for Order Processing
Let’s create some utility types to help with order processing:
type OrderSummary = Pick<
Order,
"orderNumber" | "status" | "totals" | "customer"
>;
type OrderUpdate = Partial<Omit<Order, "id" | "createdAt" | "orderNumber">>;
const isValidStatusTransition = (
currentStatus: OrderStatus,
newStatus: OrderStatus
): boolean => {
const validTransitions: Record<OrderStatus, OrderStatus[]> = {
pending: ["processing", "cancelled"],
processing: ["shipped", "cancelled"],
shipped: ["delivered", "cancelled"],
delivered: [],
cancelled: [],
};
return validTransitions[currentStatus]?.includes(newStatus) ?? false;
};
class OrderProcessor {
private eventEmitter: TypedEventEmitter<OrderEvents>;
async updateOrderStatus(
order: Order,
newStatus: OrderStatus
): Promise<Result<Order, Error>> {
if (!isValidStatusTransition(order.status, newStatus)) {
return Result.fail(
new Error(`Invalid status transition: ${order.status} -> ${newStatus}`)
);
}
const updatedOrder: Order = {
...order,
status: newStatus,
updatedAt: new Date(),
};
this.eventEmitter.emit("order.updated", {
order: updatedOrder,
changes: { status: newStatus },
timestamp: new Date(),
});
return Result.ok(updatedOrder);
}
}
Generic Result Type for Error Handling
Here’s a type-safe way to handle operations that might fail:
class Result<T, E extends Error> {
private constructor(
private readonly value: T | null,
private readonly error: E | null
) {}
static ok<T, E extends Error>(value: T): Result<T, E> {
return new Result(value, null);
}
static fail<T, E extends Error>(error: E): Result<T, E> {
return new Result(null, error);
}
isOk(): boolean {
return this.error === null;
}
isFail(): boolean {
return this.error !== null;
}
unwrap(): T {
if (this.error) {
throw this.error;
}
return this.value!;
}
unwrapOr(defaultValue: T): T {
return this.value ?? defaultValue;
}
}
Practical Usage
Here’s how we can use these types in a real application:
async function processOrder(order: Order): Promise<Result<Order, Error>> {
const processor = new OrderProcessor();
const eventEmitter = new TypedEventEmitter<OrderEvents>();
eventEmitter.on("order.updated", async (payload) => {
await notifyCustomer(payload.order);
await updateInventory(payload.order);
});
try {
if (!validateOrder(order)) {
return Result.fail(new Error("Invalid order"));
}
const paymentResult = await processPayment(order);
if (!paymentResult.isOk()) {
eventEmitter.emit("order.payment_failed", {
order,
payment: order.payment,
error: {
code: "PAYMENT_FAILED",
message: paymentResult.unwrapOr({ message: "Unknown error" }).message,
},
});
return Result.fail(new Error("Payment failed"));
}
return await processor.updateOrderStatus(order, "processing");
} catch (error) {
return Result.fail(error instanceof Error ? error : new Error("Unknown error"));
}
}
This example demonstrates several advanced TypeScript patterns:
- Complex nested object types with strict type safety
- Generic type parameters for flexible yet type-safe implementations
- Discriminated unions for state management
- Type guards for runtime type checking
- Utility types for type manipulation
- Event system with typed payloads
- Result type for error handling
These patterns help create maintainable and type-safe applications while handling real-world complexity. The combination of strict typing and flexible generics allows us to build robust systems that catch errors at compile time rather than runtime.
Remember that TypeScript’s type system is not just about adding types - it’s about modeling your domain in a way that prevents invalid states and makes invalid operations impossible at the type level.