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 User

Exercise 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 shipped

Exercise 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

  1. Start Simple: Begin with basic types, then add complexity
  2. Use Playground: Use TypeScript Playground for quick experiments
  3. Read Errors: TypeScript errors are verbose but informative
  4. Leverage IntelliSense: Let your IDE guide you
  5. Type First: Write types before implementation
  6. Test Types: Use type-level tests with expectType
  7. 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.

🧭 Quick Navigation

← Healthcare Platform | Back to Scenarios | Project Templates →