Workshop Interactive Exercises
Exercise 1: Type-Safe API Client
// Exercise: Create a type-safe API client that:
// 1. Automatically infers response types from endpoint definitions
// 2. Validates request/response with Zod
// 3. Handles errors gracefully
// 4. Supports request cancellation
// TODO: Define the API endpoint types
type ApiEndpoints = {
'/users': {
GET: {
params: { page?: number; limit?: number };
response: { users: User[]; total: number };
};
POST: {
body: CreateUserDto;
response: User;
};
};
'/users/:id': {
GET: {
params: { id: string };
response: User;
};
PUT: {
params: { id: string };
body: UpdateUserDto;
response: User;
};
DELETE: {
params: { id: string };
response: { success: boolean };
};
};
};
// TODO: Implement the type-safe client
class TypeSafeApiClient {
// Your implementation here
}
// Usage should look like:
const client = new TypeSafeApiClient('https://api.example.com');
// TypeScript should infer the correct types
const users = await client.get('/users', { params: { page: 1 } });
// users is typed as { users: User[]; total: number }
const newUser = await client.post('/users', {
body: { name: 'John', email: 'john@example.com' }
});
// newUser is typed as UserExercise 2: State Machine with TypeScript
// Exercise: Implement a type-safe state machine for an order workflow
// Requirements:
// 1. States: draft, pending, processing, shipped, delivered, cancelled
// 2. Valid transitions only (e.g., can't go from shipped to draft)
// 3. State-specific data (e.g., tracking number only in shipped state)
// 4. Type-safe event handling
type OrderState =
| { status: 'draft'; items: OrderItem[] }
| { status: 'pending'; items: OrderItem[]; total: number }
| { status: 'processing'; items: OrderItem[]; total: number; paymentId: string }
| { status: 'shipped'; items: OrderItem[]; total: number; trackingNumber: string }
| { status: 'delivered'; items: OrderItem[]; total: number; deliveredAt: Date }
| { status: 'cancelled'; reason: string; cancelledAt: Date };
// TODO: Implement the state machine
class OrderStateMachine {
// Your implementation here
}
// Usage:
const order = new OrderStateMachine();
order.addItem({ productId: '123', quantity: 2 });
order.submit(); // Transitions to pending
order.processPayment('payment-123'); // Transitions to processing
order.ship('TRACK-123'); // Transitions to shipped
// order.addItem(...) // Should be a TypeScript error - can't add items when shippedExercise 3: Advanced Error Handling
// Exercise: Create a Result type with proper error handling
// Requirements:
// 1. Result<T, E> type that represents success or failure
// 2. Chain operations with map, flatMap
// 3. Proper error recovery
// 4. Async support
// TODO: Implement Result type and utilities
type Result<T, E> = // Your implementation
// TODO: Implement these utility functions
function ok<T>(value: T): Result<T, never> { }
function err<E>(error: E): Result<never, E> { }
// Usage example:
async function fetchUserWithPosts(userId: string): Promise<Result<UserWithPosts, ApiError>> {
const userResult = await fetchUser(userId);
return userResult
.flatMap(async (user) => {
const postsResult = await fetchUserPosts(user.id);
return postsResult.map(posts => ({ ...user, posts }));
})
.mapError(error => ({
...error,
context: `Failed to fetch user ${userId} with posts`,
}));
}
// Handle the result
const result = await fetchUserWithPosts('123');
if (result.isOk()) {
console.log('User:', result.value);
} else {
console.error('Error:', result.error);
}Exercise 4: Type-Safe Event Emitter
// Exercise: Build a type-safe event emitter
// Requirements:
// 1. Strongly typed events
// 2. Automatic inference of event payloads
// 3. Support for wildcard listeners
// 4. Proper cleanup
// TODO: Define your event types
type AppEvents = {
'user:login': { userId: string; timestamp: Date };
'user:logout': { userId: string; reason?: string };
'order:created': { orderId: string; userId: string; total: number };
'order:updated': { orderId: string; changes: Partial<Order> };
};
// TODO: Implement the TypedEventEmitter
class TypedEventEmitter<TEvents extends Record<string, any>> {
// Your implementation here
}
// Usage:
const emitter = new TypedEventEmitter<AppEvents>();
// TypeScript knows the payload type
emitter.on('user:login', ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
// Error if wrong payload
emitter.emit('user:login', { userId: '123' }); // TS Error: missing timestamp
// Wildcard support
emitter.on('order:*', (event, payload) => {
console.log(`Order event: ${event}`, payload);
});Exercise 5: Monorepo Package Builder
// Exercise: Create a build system for TypeScript monorepo
// Requirements:
// 1. Detect package dependencies
// 2. Build in correct order
// 3. Watch mode with incremental builds
// 4. Cache build results
// TODO: Define types for the build system
interface Package {
name: string;
path: string;
dependencies: string[];
devDependencies: string[];
scripts: Record<string, string>;
}
interface BuildConfig {
packages: Package[];
tsConfig: string;
outDir: string;
cache?: boolean;
}
// TODO: Implement the builder
class MonorepoBuilder {
constructor(private config: BuildConfig) {}
async build(): Promise<BuildResult> {
// Your implementation
}
async watch(): Promise<void> {
// Your implementation
}
private detectChanges(): string[] {
// Your implementation
}
private buildPackage(pkg: Package): Promise<void> {
// Your implementation
}
}
// Usage:
const builder = new MonorepoBuilder({
packages: await detectPackages('./packages'),
tsConfig: './tsconfig.json',
outDir: './dist',
cache: true,
});
await builder.build();
// or
await builder.watch();Workshop Tips
- Start Simple: Begin with basic types, then add complexity
- Use Playground: Use TypeScript Playground for quick experiments
- Read Errors: TypeScript errors are verbose but informative
- Leverage IntelliSense: Let your IDE guide you
- Type First: Write types before implementation
- Test Types: Use type-level tests with
expectType - Document Intent: Use JSDoc for complex type utilities
Solutions
Solutions are available in the solutions branch of the workshop repository. Try to complete each exercise before checking the solutions!
Additional Challenges
Challenge 1: Type-Safe SQL Query Builder
Build a query builder that provides type safety for SQL queries based on your schema.
Challenge 2: React Component Library
Create a type-safe component library with proper prop inference and composition patterns.
Challenge 3: GraphQL Client
Implement a GraphQL client that infers types from your schema and provides auto-completion.
Related Resources
🧭 Quick Navigation
← Healthcare Platform | Back to Scenarios | Project Templates →