Introduction
Building on our foundation from Part 1, we'll dive into more advanced TypeScript features that help you write more robust and maintainable code. This guide focuses on practical patterns and real-world scenarios.
Advanced Type Concepts
1. Union and Intersection Types
// Union Types
type StringOrNumber = string | number;
type Status = "success" | "error" | "pending";
// Intersection Types
type Employee = {
id: number;
name: string;
};
type Manager = {
subordinates: number;
department: string;
};
type ManagerEmployee = Employee & Manager;
const seniorManager: ManagerEmployee = {
id: 1,
name: "John Doe",
subordinates: 5,
department: "Engineering"
};
2. Generics
// Generic Functions
function wrapInArray<T>(value: T): T[] {
return [value];
}
// Generic Interfaces
interface Repository<T> {
get(id: string): Promise<T>;
save(item: T): Promise<void>;
delete(id: string): Promise<boolean>;
}
// Generic Classes
class Queue<T> {
private data: T[] = [];
push(item: T): void {
this.data.push(item);
}
pop(): T | undefined {
return this.data.shift();
}
}
3. Type Guards and Type Narrowing
// Type Guards
function isString(value: unknown): value is string {
return typeof value === "string";
}
// Using type guards
function processValue(value: string | number) {
if (isString(value)) {
console.log(value.toUpperCase()); // TypeScript knows value is string
} else {
console.log(value.toFixed(2)); // TypeScript knows value is number
}
}
// Discriminated Unions
type Circle = {
kind: "circle";
radius: number;
};
type Rectangle = {
kind: "rectangle";
width: number;
height: number;
};
type Shape = Circle | Rectangle;
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
}
}
Advanced Design Patterns
1. Builder Pattern with Method Chaining
class QueryBuilder {
private query: string = '';
private parameters: any[] = [];
select(columns: string[]): this {
this.query = `SELECT ${columns.join(', ')}`;
return this;
}
from(table: string): this {
this.query += ` FROM ${table}`;
return this;
}
where(condition: string, parameter: any): this {
this.query += ` WHERE ${condition}`;
this.parameters.push(parameter);
return this;
}
build(): { query: string; parameters: any[] } {
return {
query: this.query,
parameters: this.parameters
};
}
}
// Usage
const query = new QueryBuilder()
.select(['name', 'email'])
.from('users')
.where('age > ?', 18)
.build();
2. Factory Pattern with Abstract Classes
abstract class Payment {
abstract process(amount: number): void;
}
class CreditCardPayment extends Payment {
process(amount: number): void {
console.log(`Processing credit card payment: $${amount}`);
}
}
class PayPalPayment extends Payment {
process(amount: number): void {
console.log(`Processing PayPal payment: $${amount}`);
}
}
class PaymentFactory {
static createPayment(type: "creditcard" | "paypal"): Payment {
switch (type) {
case "creditcard":
return new CreditCardPayment();
case "paypal":
return new PayPalPayment();
default:
throw new Error("Invalid payment type");
}
}
}
3. Decorators (Experimental Feature)
// Method Decorator
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with:`, args);
const result = originalMethod.apply(this, args);
console.log(`Result:`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
Practical Project: Task Management System
Let's build a more complex project incorporating these concepts:
// Types and Interfaces
interface User {
id: string;
name: string;
email: string;
}
interface Task {
id: string;
title: string;
description: string;
status: TaskStatus;
assignee?: User;
dueDate: Date;
}
type TaskStatus = "TODO" | "IN_PROGRESS" | "DONE";
// Generic Repository Implementation
class TaskRepository implements Repository<Task> {
private tasks: Map<string, Task> = new Map();
async get(id: string): Promise<Task> {
const task = this.tasks.get(id);
if (!task) throw new Error("Task not found");
return task;
}
async save(task: Task): Promise<void> {
this.tasks.set(task.id, task);
}
async delete(id: string): Promise<boolean> {
return this.tasks.delete(id);
}
}
// Task Management Service
class TaskManagementService {
constructor(private repository: Repository<Task>) {}
async assignTask(taskId: string, user: User): Promise<void> {
const task = await this.repository.get(taskId);
task.assignee = user;
await this.repository.save(task);
}
async updateStatus(taskId: string, status: TaskStatus): Promise<void> {
const task = await this.repository.get(taskId);
task.status = status;
await this.repository.save(task);
}
}
Best Practices and Tips
- Use strict compiler options:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true
}
}
- Leverage type inference when possible
- Use readonly properties for immutability
- Consider using branded types for type safety
- Make good use of discriminated unions
- Implement proper error handling with custom error types
Next Steps
- Explore advanced type manipulation
- Study real-world TypeScript codebases
- Practice implementing design patterns
- Learn about module augmentation
Resources
- TypeScript Design Patterns Book
- Advanced TypeScript GitHub repositories
- TypeScript compiler API documentation
Top comments (0)