Interactive TypeScript Workshop Exercises - CLAUDE.md
Exercise Structure & Guidelines
How to Use These Exercises
- Each exercise has a Problem, Starter Code, and Hints
- Try to solve without looking at hints first
- Run tests to verify your solution
- Compare with the provided solution
- Experiment with variations
Setting Up Your Environment
# Initialize exercise environment
npm init -y
npm install -D typescript @types/node jest @types/jest ts-jest
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
# TypeScript config for exercises
cat > tsconfig.json << EOF
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
EOF
# Jest config
cat > jest.config.js << EOF
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.test.ts'],
};
EOFExercise 1: Generic Type Constraints
Problem
Create a type-safe configuration system that:
- Validates configuration values
- Provides auto-completion
- Prevents invalid configurations
- Supports nested objects
Starter Code
// src/exercise1.ts
type ConfigSchema = {
// TODO: Define your schema type
};
class ConfigBuilder<T extends ConfigSchema> {
// TODO: Implement the builder
set<K extends keyof T>(key: K, value: T[K]): this {
// TODO: Implement
throw new Error('Not implemented');
}
get<K extends keyof T>(key: K): T[K] | undefined {
// TODO: Implement
throw new Error('Not implemented');
}
validate(): boolean {
// TODO: Implement validation
throw new Error('Not implemented');
}
build(): Readonly<T> {
// TODO: Return validated config
throw new Error('Not implemented');
}
}
// Usage example (should work after implementation):
interface AppConfig {
server: {
port: number;
host: string;
ssl: boolean;
};
database: {
url: string;
poolSize: number;
};
features: {
authentication: boolean;
rateLimit: {
enabled: boolean;
maxRequests: number;
};
};
}
const config = new ConfigBuilder<AppConfig>()
.set('server', { port: 3000, host: 'localhost', ssl: false })
.set('database', { url: 'postgres://localhost', poolSize: 10 })
.set('features', {
authentication: true,
rateLimit: { enabled: true, maxRequests: 100 }
})
.build();
// Tests
export { ConfigBuilder };Test File
// src/exercise1.test.ts
import { ConfigBuilder } from './exercise1';
interface TestConfig {
name: string;
value: number;
nested: {
enabled: boolean;
};
}
describe('ConfigBuilder', () => {
it('should set and get values', () => {
const builder = new ConfigBuilder<TestConfig>();
builder.set('name', 'test');
expect(builder.get('name')).toBe('test');
});
it('should handle nested objects', () => {
const builder = new ConfigBuilder<TestConfig>();
builder.set('nested', { enabled: true });
expect(builder.get('nested')).toEqual({ enabled: true });
});
it('should return readonly config', () => {
const builder = new ConfigBuilder<TestConfig>();
builder
.set('name', 'test')
.set('value', 42)
.set('nested', { enabled: false });
const config = builder.build();
// @ts-expect-error - should be readonly
config.name = 'changed';
});
it('should validate required fields', () => {
const builder = new ConfigBuilder<TestConfig>();
builder.set('name', 'test');
expect(builder.validate()).toBe(false); // Missing required fields
});
});Hints
Click for hints
- Use
Partial<T>to store incomplete configuration - Use
keyof TandT[K]for type-safe property access - Consider using
Required<T>for validation - Use type guards to check if all required fields are set
Solution
Click for solution
type ConfigSchema = Record<string, any>;
class ConfigBuilder<T extends ConfigSchema> {
private config: Partial<T> = {};
private requiredKeys: Set<keyof T> = new Set();
constructor(required?: (keyof T)[]) {
if (required) {
required.forEach(key => this.requiredKeys.add(key));
}
}
set<K extends keyof T>(key: K, value: T[K]): this {
this.config[key] = value;
return this;
}
get<K extends keyof T>(key: K): T[K] | undefined {
return this.config[key];
}
validate(): boolean {
// Check if all required keys are present
for (const key of this.requiredKeys) {
if (!(key in this.config)) {
return false;
}
}
// Check if all keys in config are valid
const allKeys = Object.keys(this.config);
const validKeys = this.getAllKeys();
return allKeys.every(key => validKeys.includes(key));
}
private getAllKeys(): string[] {
// In a real implementation, this would use the schema
return Object.keys(this.config);
}
build(): Readonly<T> {
if (!this.validate()) {
throw new Error('Configuration is invalid or incomplete');
}
return Object.freeze({ ...this.config }) as Readonly<T>;
}
}Exercise 2: Advanced Type Inference
Problem
Create a fluent API query builder that:
- Infers types from method chains
- Prevents invalid query combinations
- Provides compile-time safety
- Supports multiple database operations
Starter Code
// src/exercise2.ts
type QueryState = {
select?: string[];
from?: string;
where?: Array<{ field: string; op: string; value: any }>;
orderBy?: Array<{ field: string; direction: 'ASC' | 'DESC' }>;
limit?: number;
};
class QueryBuilder<T = unknown> {
// TODO: Implement the query builder with proper type inference
select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>> {
// TODO: Implement
throw new Error('Not implemented');
}
from(table: string): this {
// TODO: Implement
throw new Error('Not implemented');
}
where<K extends keyof T>(
field: K,
op: '=' | '!=' | '>' | '<' | '>=' | '<=',
value: T[K]
): this {
// TODO: Implement
throw new Error('Not implemented');
}
orderBy<K extends keyof T>(
field: K,
direction: 'ASC' | 'DESC' = 'ASC'
): this {
// TODO: Implement
throw new Error('Not implemented');
}
limit(count: number): this {
// TODO: Implement
throw new Error('Not implemented');
}
build(): string {
// TODO: Build SQL query string
throw new Error('Not implemented');
}
}
// Usage example:
interface User {
id: number;
name: string;
email: string;
age: number;
createdAt: Date;
}
const query = new QueryBuilder<User>()
.select('id', 'name', 'email')
.from('users')
.where('age', '>=', 18)
.where('email', '!=', '')
.orderBy('createdAt', 'DESC')
.limit(10)
.build();
console.log(query);
// Should output: SELECT id, name, email FROM users WHERE age >= 18 AND email != '' ORDER BY createdAt DESC LIMIT 10
export { QueryBuilder };Test File
// src/exercise2.test.ts
import { QueryBuilder } from './exercise2';
interface TestUser {
id: number;
name: string;
email: string;
active: boolean;
}
describe('QueryBuilder', () => {
it('should build basic select query', () => {
const query = new QueryBuilder<TestUser>()
.select('id', 'name')
.from('users')
.build();
expect(query).toBe('SELECT id, name FROM users');
});
it('should handle where clauses', () => {
const query = new QueryBuilder<TestUser>()
.select('id', 'name')
.from('users')
.where('active', '=', true)
.build();
expect(query).toContain('WHERE active = true');
});
it('should chain multiple where clauses', () => {
const query = new QueryBuilder<TestUser>()
.select('id')
.from('users')
.where('active', '=', true)
.where('name', '!=', '')
.build();
expect(query).toContain('WHERE active = true AND name != \'\'');
});
it('should type-check field names', () => {
const builder = new QueryBuilder<TestUser>();
// This should compile
builder.where('email', '=', 'test@example.com');
// @ts-expect-error - invalid field
builder.where('invalid', '=', 'value');
});
it('should type-check values', () => {
const builder = new QueryBuilder<TestUser>();
// @ts-expect-error - wrong type for boolean field
builder.where('active', '=', 'not a boolean');
});
});Hints
Click for hints
- Store query state internally
- Use method chaining by returning
this - For
select, return a new QueryBuilder with transformed type - Use template literal types for SQL building
- Escape values properly in the build method
Solution
Click for solution
type QueryState = {
select?: string[];
from?: string;
where?: Array<{ field: string; op: string; value: any }>;
orderBy?: Array<{ field: string; direction: 'ASC' | 'DESC' }>;
limit?: number;
};
class QueryBuilder<T = unknown> {
private state: QueryState = {};
select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>> {
this.state.select = fields as string[];
return this as any;
}
from(table: string): this {
this.state.from = table;
return this;
}
where<K extends keyof T>(
field: K,
op: '=' | '!=' | '>' | '<' | '>=' | '<=',
value: T[K]
): this {
if (!this.state.where) {
this.state.where = [];
}
this.state.where.push({
field: field as string,
op,
value,
});
return this;
}
orderBy<K extends keyof T>(
field: K,
direction: 'ASC' | 'DESC' = 'ASC'
): this {
if (!this.state.orderBy) {
this.state.orderBy = [];
}
this.state.orderBy.push({
field: field as string,
direction,
});
return this;
}
limit(count: number): this {
this.state.limit = count;
return this;
}
build(): string {
const parts: string[] = [];
// SELECT clause
if (this.state.select && this.state.select.length > 0) {
parts.push(`SELECT ${this.state.select.join(', ')}`);
} else {
parts.push('SELECT *');
}
// FROM clause
if (this.state.from) {
parts.push(`FROM ${this.state.from}`);
}
// WHERE clause
if (this.state.where && this.state.where.length > 0) {
const conditions = this.state.where.map(({ field, op, value }) => {
const escapedValue = typeof value === 'string'
? `'${value}'`
: value;
return `${field} ${op} ${escapedValue}`;
});
parts.push(`WHERE ${conditions.join(' AND ')}`);
}
// ORDER BY clause
if (this.state.orderBy && this.state.orderBy.length > 0) {
const ordering = this.state.orderBy
.map(({ field, direction }) => `${field} ${direction}`)
.join(', ');
parts.push(`ORDER BY ${ordering}`);
}
// LIMIT clause
if (this.state.limit !== undefined) {
parts.push(`LIMIT ${this.state.limit}`);
}
return parts.join(' ');
}
}Exercise 3: Async Error Handling Pattern
Problem
Implement a resilient async operation handler that:
- Retries failed operations with exponential backoff
- Supports circuit breaker pattern
- Provides detailed error information
- Allows custom retry strategies
Starter Code
// src/exercise3.ts
interface RetryOptions {
maxAttempts?: number;
initialDelay?: number;
maxDelay?: number;
backoffMultiplier?: number;
shouldRetry?: (error: unknown, attempt: number) => boolean;
}
interface CircuitBreakerOptions {
failureThreshold?: number;
resetTimeout?: number;
halfOpenRequests?: number;
}
type AsyncOperation<T> = () => Promise<T>;
class ResilientAsync<T> {
// TODO: Implement resilient async operations
static async retry<T>(
operation: AsyncOperation<T>,
options?: RetryOptions
): Promise<T> {
// TODO: Implement retry logic
throw new Error('Not implemented');
}
static circuitBreaker<T>(
operation: AsyncOperation<T>,
options?: CircuitBreakerOptions
): AsyncOperation<T> {
// TODO: Implement circuit breaker
throw new Error('Not implemented');
}
static async withTimeout<T>(
operation: AsyncOperation<T>,
timeoutMs: number
): Promise<T> {
// TODO: Implement timeout
throw new Error('Not implemented');
}
static async resilient<T>(
operation: AsyncOperation<T>,
options?: {
retry?: RetryOptions;
circuitBreaker?: CircuitBreakerOptions;
timeout?: number;
}
): Promise<T> {
// TODO: Combine all patterns
throw new Error('Not implemented');
}
}
// Usage example:
async function unreliableApiCall(): Promise<string> {
if (Math.random() > 0.7) {
return 'Success!';
}
throw new Error('API call failed');
}
// Should retry up to 3 times with exponential backoff
const result = await ResilientAsync.retry(unreliableApiCall, {
maxAttempts: 3,
initialDelay: 100,
backoffMultiplier: 2,
});
export { ResilientAsync, RetryOptions, CircuitBreakerOptions };Test File
// src/exercise3.test.ts
import { ResilientAsync } from './exercise3';
describe('ResilientAsync', () => {
describe('retry', () => {
it('should retry failed operations', async () => {
let attempts = 0;
const operation = async () => {
attempts++;
if (attempts < 3) {
throw new Error('Failed');
}
return 'Success';
};
const result = await ResilientAsync.retry(operation, {
maxAttempts: 3,
});
expect(result).toBe('Success');
expect(attempts).toBe(3);
});
it('should respect max attempts', async () => {
const operation = async () => {
throw new Error('Always fails');
};
await expect(
ResilientAsync.retry(operation, { maxAttempts: 2 })
).rejects.toThrow('Always fails');
});
it('should use exponential backoff', async () => {
const delays: number[] = [];
let lastCall = Date.now();
const operation = async () => {
const now = Date.now();
delays.push(now - lastCall);
lastCall = now;
throw new Error('Failed');
};
try {
await ResilientAsync.retry(operation, {
maxAttempts: 3,
initialDelay: 100,
backoffMultiplier: 2,
});
} catch {}
// First delay should be ~100ms, second ~200ms
expect(delays[1]).toBeGreaterThan(90);
expect(delays[2]).toBeGreaterThan(180);
});
});
describe('circuitBreaker', () => {
it('should open circuit after failures', async () => {
let calls = 0;
const operation = async () => {
calls++;
throw new Error('Failed');
};
const protected = ResilientAsync.circuitBreaker(operation, {
failureThreshold: 2,
});
// First two calls should execute
await expect(protected()).rejects.toThrow();
await expect(protected()).rejects.toThrow();
// Circuit should be open, no more calls
await expect(protected()).rejects.toThrow('Circuit breaker is open');
expect(calls).toBe(2);
});
});
describe('withTimeout', () => {
it('should timeout long operations', async () => {
const operation = async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
return 'Done';
};
await expect(
ResilientAsync.withTimeout(operation, 100)
).rejects.toThrow('Operation timed out');
});
});
});Hints
Click for hints
- For retry: Use a loop with try-catch
- For backoff: Calculate delay as
initialDelay * (backoffMultiplier ** attempt) - For circuit breaker: Track state (closed, open, half-open)
- For timeout: Use
Promise.racewith a timeout promise - Consider using
AbortControllerfor cancellation
Solution
Click for solution
interface RetryOptions {
maxAttempts?: number;
initialDelay?: number;
maxDelay?: number;
backoffMultiplier?: number;
shouldRetry?: (error: unknown, attempt: number) => boolean;
}
interface CircuitBreakerOptions {
failureThreshold?: number;
resetTimeout?: number;
halfOpenRequests?: number;
}
type AsyncOperation<T> = () => Promise<T>;
enum CircuitState {
CLOSED = 'CLOSED',
OPEN = 'OPEN',
HALF_OPEN = 'HALF_OPEN',
}
class ResilientAsync<T> {
static async retry<T>(
operation: AsyncOperation<T>,
options?: RetryOptions
): Promise<T> {
const {
maxAttempts = 3,
initialDelay = 100,
maxDelay = 10000,
backoffMultiplier = 2,
shouldRetry = () => true,
} = options || {};
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === maxAttempts - 1 || !shouldRetry(error, attempt)) {
throw error;
}
const delay = Math.min(
initialDelay * Math.pow(backoffMultiplier, attempt),
maxDelay
);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
static circuitBreaker<T>(
operation: AsyncOperation<T>,
options?: CircuitBreakerOptions
): AsyncOperation<T> {
const {
failureThreshold = 5,
resetTimeout = 60000,
halfOpenRequests = 1,
} = options || {};
let state = CircuitState.CLOSED;
let failures = 0;
let lastFailureTime = 0;
let halfOpenAttempts = 0;
return async () => {
// Check if circuit should be reset
if (
state === CircuitState.OPEN &&
Date.now() - lastFailureTime > resetTimeout
) {
state = CircuitState.HALF_OPEN;
halfOpenAttempts = 0;
}
if (state === CircuitState.OPEN) {
throw new Error('Circuit breaker is open');
}
if (state === CircuitState.HALF_OPEN && halfOpenAttempts >= halfOpenRequests) {
throw new Error('Circuit breaker is half-open, too many requests');
}
try {
if (state === CircuitState.HALF_OPEN) {
halfOpenAttempts++;
}
const result = await operation();
// Success - reset circuit
if (state === CircuitState.HALF_OPEN) {
state = CircuitState.CLOSED;
failures = 0;
}
return result;
} catch (error) {
failures++;
lastFailureTime = Date.now();
if (failures >= failureThreshold || state === CircuitState.HALF_OPEN) {
state = CircuitState.OPEN;
}
throw error;
}
};
}
static async withTimeout<T>(
operation: AsyncOperation<T>,
timeoutMs: number
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Operation timed out')), timeoutMs);
});
return Promise.race([operation(), timeoutPromise]);
}
static async resilient<T>(
operation: AsyncOperation<T>,
options?: {
retry?: RetryOptions;
circuitBreaker?: CircuitBreakerOptions;
timeout?: number;
}
): Promise<T> {
let finalOperation = operation;
if (options?.timeout) {
const timeout = options.timeout;
const originalOp = finalOperation;
finalOperation = () => this.withTimeout(originalOp, timeout);
}
if (options?.circuitBreaker) {
finalOperation = this.circuitBreaker(finalOperation, options.circuitBreaker);
}
if (options?.retry) {
return this.retry(finalOperation, options.retry);
}
return finalOperation();
}
}Exercise 4: Type-Safe State Management
Problem
Build a Redux-like state management system that:
- Provides full type safety
- Supports middleware
- Has time-travel debugging
- Allows modular reducers
Starter Code
// src/exercise4.ts
type Action<T = any> = {
type: string;
payload?: T;
};
type Reducer<S, A extends Action> = (state: S, action: A) => S;
type Middleware<S> = (
store: Store<S>
) => (next: Dispatch) => (action: Action) => any;
type Dispatch = (action: Action) => void;
interface Store<S> {
getState(): S;
dispatch: Dispatch;
subscribe(listener: () => void): () => void;
}
class TypedStore<S, A extends Action = Action> implements Store<S> {
// TODO: Implement the store
constructor(
reducer: Reducer<S, A>,
initialState: S,
middleware?: Middleware<S>[]
) {
// TODO: Initialize store
throw new Error('Not implemented');
}
getState(): S {
// TODO: Return current state
throw new Error('Not implemented');
}
dispatch(action: A): void {
// TODO: Dispatch action
throw new Error('Not implemented');
}
subscribe(listener: () => void): () => void {
// TODO: Subscribe to changes
throw new Error('Not implemented');
}
// Time travel methods
getHistory(): Array<{ state: S; action: A }> {
// TODO: Get state history
throw new Error('Not implemented');
}
jumpToState(index: number): void {
// TODO: Jump to historical state
throw new Error('Not implemented');
}
}
// Usage example:
interface CounterState {
count: number;
lastAction?: string;
}
type CounterAction =
| { type: 'INCREMENT'; payload?: number }
| { type: 'DECREMENT'; payload?: number }
| { type: 'RESET' };
const counterReducer: Reducer<CounterState, CounterAction> = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + (action.payload || 1),
lastAction: 'INCREMENT'
};
case 'DECREMENT':
return {
count: state.count - (action.payload || 1),
lastAction: 'DECREMENT'
};
case 'RESET':
return { count: 0, lastAction: 'RESET' };
default:
return state;
}
};
// Logger middleware
const logger: Middleware<CounterState> = store => next => action => {
console.log('Action:', action);
const result = next(action);
console.log('New state:', store.getState());
return result;
};
const store = new TypedStore(
counterReducer,
{ count: 0 },
[logger]
);
export { TypedStore, Reducer, Middleware, Action };Test File
// src/exercise4.test.ts
import { TypedStore } from './exercise4';
interface TestState {
value: number;
history: string[];
}
type TestAction =
| { type: 'ADD'; payload: number }
| { type: 'MULTIPLY'; payload: number }
| { type: 'RESET' };
const testReducer = (state: TestState, action: TestAction): TestState => {
switch (action.type) {
case 'ADD':
return {
value: state.value + action.payload,
history: [...state.history, `ADD ${action.payload}`],
};
case 'MULTIPLY':
return {
value: state.value * action.payload,
history: [...state.history, `MULTIPLY ${action.payload}`],
};
case 'RESET':
return { value: 0, history: [] };
default:
return state;
}
};
describe('TypedStore', () => {
it('should handle basic dispatch', () => {
const store = new TypedStore(
testReducer,
{ value: 0, history: [] }
);
store.dispatch({ type: 'ADD', payload: 5 });
expect(store.getState().value).toBe(5);
store.dispatch({ type: 'MULTIPLY', payload: 3 });
expect(store.getState().value).toBe(15);
});
it('should notify subscribers', () => {
const store = new TypedStore(
testReducer,
{ value: 0, history: [] }
);
const listener = jest.fn();
const unsubscribe = store.subscribe(listener);
store.dispatch({ type: 'ADD', payload: 1 });
expect(listener).toHaveBeenCalledTimes(1);
unsubscribe();
store.dispatch({ type: 'ADD', payload: 1 });
expect(listener).toHaveBeenCalledTimes(1);
});
it('should apply middleware', () => {
const middleware = jest.fn(store => next => action => {
if (action.type === 'ADD') {
action.payload = action.payload * 2;
}
return next(action);
});
const store = new TypedStore(
testReducer,
{ value: 0, history: [] },
[middleware]
);
store.dispatch({ type: 'ADD', payload: 5 });
expect(store.getState().value).toBe(10); // Doubled by middleware
});
it('should support time travel', () => {
const store = new TypedStore(
testReducer,
{ value: 0, history: [] }
);
store.dispatch({ type: 'ADD', payload: 5 });
store.dispatch({ type: 'MULTIPLY', payload: 3 });
store.dispatch({ type: 'ADD', payload: 10 });
const history = store.getHistory();
expect(history).toHaveLength(4); // Initial + 3 actions
store.jumpToState(1); // After first ADD
expect(store.getState().value).toBe(5);
store.jumpToState(2); // After MULTIPLY
expect(store.getState().value).toBe(15);
});
});Hints
Click for hints
- Store state history as array of snapshots
- Apply middleware by composing functions
- Use closure for subscribe/unsubscribe pattern
- Deep clone state to prevent mutations
- For time travel, replay actions up to index
Solution
Click for solution
type Action<T = any> = {
type: string;
payload?: T;
};
type Reducer<S, A extends Action> = (state: S, action: A) => S;
type Middleware<S> = (
store: Store<S>
) => (next: Dispatch) => (action: Action) => any;
type Dispatch = (action: Action) => void;
interface Store<S> {
getState(): S;
dispatch: Dispatch;
subscribe(listener: () => void): () => void;
}
class TypedStore<S, A extends Action = Action> implements Store<S> {
private state: S;
private reducer: Reducer<S, A>;
private listeners: Set<() => void> = new Set();
private history: Array<{ state: S; action?: A }> = [];
private dispatch_: Dispatch;
constructor(
reducer: Reducer<S, A>,
initialState: S,
middleware?: Middleware<S>[]
) {
this.reducer = reducer;
this.state = initialState;
// Store initial state in history
this.history.push({ state: this.deepClone(initialState) });
// Set up dispatch with middleware
let dispatch: Dispatch = this.baseDispatch.bind(this);
if (middleware) {
// Apply middleware in reverse order
for (let i = middleware.length - 1; i >= 0; i--) {
dispatch = middleware[i](this)(dispatch);
}
}
this.dispatch_ = dispatch;
}
getState(): S {
return this.deepClone(this.state);
}
dispatch(action: A): void {
this.dispatch_(action);
}
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
getHistory(): Array<{ state: S; action: A }> {
// Skip the initial state (no action)
return this.history.slice(1) as Array<{ state: S; action: A }>;
}
jumpToState(index: number): void {
if (index < 0 || index >= this.history.length) {
throw new Error('Invalid history index');
}
this.state = this.deepClone(this.history[index].state);
// Truncate history to this point
this.history = this.history.slice(0, index + 1);
// Notify listeners
this.notifyListeners();
}
private baseDispatch(action: A): void {
// Apply reducer
const newState = this.reducer(this.state, action);
// Only update if state changed
if (newState !== this.state) {
this.state = newState;
// Add to history
this.history.push({
state: this.deepClone(newState),
action,
});
// Notify listeners
this.notifyListeners();
}
}
private notifyListeners(): void {
this.listeners.forEach(listener => listener());
}
private deepClone(obj: S): S {
return JSON.parse(JSON.stringify(obj));
}
}Exercise 5: Advanced Type Utilities
Problem
Create a set of advanced TypeScript utilities that:
- Deep partial/required types
- Path-based property access
- Type-safe object merging
- Conditional type mapping
Starter Code
// src/exercise5.ts
// TODO: Implement DeepPartial - makes all properties optional recursively
type DeepPartial<T> = any; // Replace with implementation
// TODO: Implement DeepRequired - makes all properties required recursively
type DeepRequired<T> = any; // Replace with implementation
// TODO: Implement Paths - gets all possible paths in an object
type Paths<T> = any; // Replace with implementation
// TODO: Implement PathValue - gets the type at a specific path
type PathValue<T, P extends Paths<T>> = any; // Replace with implementation
// TODO: Implement DeepMerge - merges two types deeply
type DeepMerge<T, U> = any; // Replace with implementation
// TODO: Implement ConditionalKeys - gets keys where value matches condition
type ConditionalKeys<T, Condition> = any; // Replace with implementation
// Utility functions using the types
class TypedUtils {
// TODO: Get nested property by path
static get<T, P extends Paths<T>>(
obj: T,
path: P
): PathValue<T, P> {
// TODO: Implement
throw new Error('Not implemented');
}
// TODO: Set nested property by path
static set<T, P extends Paths<T>>(
obj: T,
path: P,
value: PathValue<T, P>
): T {
// TODO: Implement
throw new Error('Not implemented');
}
// TODO: Deep merge objects
static merge<T, U>(obj1: T, obj2: U): DeepMerge<T, U> {
// TODO: Implement
throw new Error('Not implemented');
}
// TODO: Pick properties by value type
static pickByType<T, V>(
obj: T,
type: V
): Pick<T, ConditionalKeys<T, V>> {
// TODO: Implement
throw new Error('Not implemented');
}
}
// Usage examples:
interface User {
id: number;
name: string;
profile: {
age: number;
address: {
street: string;
city: string;
country: string;
};
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
};
tags: string[];
}
// DeepPartial example
type PartialUser = DeepPartial<User>;
// All properties should be optional, including nested ones
// Paths example
type UserPaths = Paths<User>;
// Should be: "id" | "name" | "profile" | "profile.age" | "profile.address" | ...
// PathValue example
type CityType = PathValue<User, 'profile.address.city'>;
// Should be: string
// Get nested value
const user: User = {
id: 1,
name: 'John',
profile: {
age: 30,
address: { street: '123 Main', city: 'NYC', country: 'USA' },
preferences: { theme: 'dark', notifications: true }
},
tags: ['developer']
};
const city = TypedUtils.get(user, 'profile.address.city');
// city should be typed as string
export { DeepPartial, DeepRequired, Paths, PathValue, DeepMerge, ConditionalKeys, TypedUtils };Test File
// src/exercise5.test.ts
import { TypedUtils } from './exercise5';
interface TestObject {
a: number;
b: {
c: string;
d: {
e: boolean;
f: number[];
};
};
g: string[];
}
describe('TypedUtils', () => {
const testObj: TestObject = {
a: 1,
b: {
c: 'hello',
d: {
e: true,
f: [1, 2, 3],
},
},
g: ['a', 'b'],
};
describe('get', () => {
it('should get nested properties', () => {
expect(TypedUtils.get(testObj, 'a')).toBe(1);
expect(TypedUtils.get(testObj, 'b.c')).toBe('hello');
expect(TypedUtils.get(testObj, 'b.d.e')).toBe(true);
expect(TypedUtils.get(testObj, 'b.d.f')).toEqual([1, 2, 3]);
});
});
describe('set', () => {
it('should set nested properties', () => {
const updated = TypedUtils.set(testObj, 'b.c', 'world');
expect(updated.b.c).toBe('world');
expect(testObj.b.c).toBe('hello'); // Original unchanged
});
it('should maintain type safety', () => {
// @ts-expect-error - wrong type
TypedUtils.set(testObj, 'b.d.e', 'not a boolean');
});
});
describe('merge', () => {
it('should deep merge objects', () => {
const obj1 = { a: 1, b: { c: 2, d: 3 } };
const obj2 = { b: { c: 4, e: 5 }, f: 6 };
const merged = TypedUtils.merge(obj1, obj2);
expect(merged).toEqual({
a: 1,
b: { c: 4, d: 3, e: 5 },
f: 6,
});
});
});
describe('pickByType', () => {
it('should pick properties by type', () => {
const obj = {
a: 1,
b: 'hello',
c: 2,
d: 'world',
e: true,
};
const numbers = TypedUtils.pickByType(obj, 0 as number);
expect(numbers).toEqual({ a: 1, c: 2 });
const strings = TypedUtils.pickByType(obj, '' as string);
expect(strings).toEqual({ b: 'hello', d: 'world' });
});
});
});Hints
Click for hints
- For DeepPartial: Use conditional types with recursion
- For Paths: Use template literal types with dot notation
- For PathValue: Split path string and traverse type
- For DeepMerge: Handle conflicts with second type taking precedence
- For ConditionalKeys: Use mapped types with conditional
Example patterns:
type IsObject<T> = T extends object ? (T extends any[] ? false : true) : false;
type Join<K, P> = K extends string | number ?
P extends string | number ? `${K}.${P}` : K : never;Solution
Click for solution
// DeepPartial - makes all properties optional recursively
type DeepPartial<T> = T extends object ? {
[P in keyof T]?: T[P] extends (infer U)[] ? DeepPartial<U>[] :
T[P] extends object ? DeepPartial<T[P]> : T[P]
} : T;
// DeepRequired - makes all properties required recursively
type DeepRequired<T> = T extends object ? {
[P in keyof T]-?: T[P] extends (infer U)[] ? DeepRequired<U>[] :
T[P] extends object | undefined ? DeepRequired<NonNullable<T[P]>> : T[P]
} : T;
// Helper to check if type is object (not array)
type IsObject<T> = T extends object ? (T extends any[] ? false : true) : false;
// Paths - gets all possible paths in an object
type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
{ [K in keyof T]-?: K extends string | number ?
`${K}` | (IsObject<T[K]> extends true ? `${K}.${Paths<T[K], Prev[D]>}` : `${K}`)
: never
}[keyof T] : "";
// Helper for recursion depth
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// PathValue - gets the type at a specific path
type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ?
K extends keyof T ? PathValue<T[K], Rest> : never :
P extends keyof T ? T[P] : never;
// DeepMerge - merges two types deeply
type DeepMerge<T, U> = T extends object ?
U extends object ?
{ [K in keyof T | keyof U]:
K extends keyof U ?
K extends keyof T ?
DeepMerge<T[K], U[K]> :
U[K] :
K extends keyof T ? T[K] : never
} : U : U;
// ConditionalKeys - gets keys where value matches condition
type ConditionalKeys<T, Condition> = {
[K in keyof T]: T[K] extends Condition ? K : never
}[keyof T];
// Utility functions using the types
class TypedUtils {
static get<T, P extends Paths<T>>(
obj: T,
path: P
): PathValue<T, P> {
const keys = path.split('.');
let result: any = obj;
for (const key of keys) {
result = result[key];
if (result === undefined) break;
}
return result;
}
static set<T, P extends Paths<T>>(
obj: T,
path: P,
value: PathValue<T, P>
): T {
const keys = path.split('.');
const result = JSON.parse(JSON.stringify(obj)); // Deep clone
let current: any = result;
for (let i = 0; i < keys.length - 1; i++) {
if (!(keys[i] in current)) {
current[keys[i]] = {};
}
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
return result;
}
static merge<T, U>(obj1: T, obj2: U): DeepMerge<T, U> {
const result: any = {};
// Copy obj1
for (const key in obj1) {
if (obj1.hasOwnProperty(key)) {
result[key] = obj1[key];
}
}
// Merge obj2
for (const key in obj2) {
if (obj2.hasOwnProperty(key)) {
if (
typeof obj2[key] === 'object' &&
obj2[key] !== null &&
!Array.isArray(obj2[key]) &&
key in result &&
typeof result[key] === 'object' &&
result[key] !== null &&
!Array.isArray(result[key])
) {
result[key] = this.merge(result[key], obj2[key]);
} else {
result[key] = obj2[key];
}
}
}
return result;
}
static pickByType<T, V>(
obj: T,
type: V
): Pick<T, ConditionalKeys<T, V>> {
const result: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key) && typeof obj[key] === typeof type) {
result[key] = obj[key];
}
}
return result;
}
}Final Challenge: Build Your Own Type-Safe ORM
Problem
Create a mini ORM (Object-Relational Mapping) that:
- Provides type-safe queries
- Supports relationships
- Has migration support
- Includes query builder
This exercise combines all concepts learned!
Starter Template
// Your challenge: implement a type-safe ORM
// Combine everything you've learned to build this
interface Model {
id: string;
createdAt: Date;
updatedAt: Date;
}
class TypedORM {
// Your implementation
}
// Goal: Support this usage
const User = TypedORM.define('users', {
id: { type: 'string', primaryKey: true },
email: { type: 'string', unique: true },
name: { type: 'string' },
age: { type: 'number' },
posts: { type: 'hasMany', model: 'Post' }
});
const Post = TypedORM.define('posts', {
id: { type: 'string', primaryKey: true },
title: { type: 'string' },
content: { type: 'text' },
authorId: { type: 'string' },
author: { type: 'belongsTo', model: 'User' }
});
// Type-safe queries
const users = await User
.where('age', '>', 18)
.include('posts')
.orderBy('createdAt', 'DESC')
.limit(10)
.execute();
// users[0].posts should be typed correctlyWorkshop Completion Checklist
- Completed all 5 main exercises
- Ran all tests successfully
- Experimented with variations
- Attempted the final challenge
- Created your own TypeScript utility
Additional Resources
- TypeScript Playground: https://www.typescriptlang.org/play
- Type Challenges: https://github.com/type-challenges/type-challenges
- TypeScript Deep Dive: https://basarat.gitbook.io/typescript/
- Utility Types Reference: https://www.typescriptlang.org/docs/handbook/utility-types.html
Next Steps
- Apply these patterns in your projects
- Create your own type utilities library
- Contribute to TypeScript definitions
- Teach others what you’ve learned
Remember: The best way to learn TypeScript is to use it in real projects!