# Chapter 7: Type Safety Paradise *"In TypeScript we trust, but in runtime we must verify."* --- ## The Type Confusion Crisis Sarah's Weather Buddy was fast, resilient, and cached perfectly. But during a code review, her new teammate Alex pointed at the screen: "What's the shape of this weather data?" Sarah squinted. "Uh... it has temp_C and... weatherDesc... I think?" "You think?" Alex pulled up the console. "Let me show you something terrifying." ```javascript // What Sarah wrote const weather = await tf.get('/api/weather/london') console.log(weather.temperature) // undefined console.log(weather.temp_C) // undefined console.log(weather.data.current_condition[0].temp_C) // 15 // 3 attempts to find the right property! ``` "This," Alex said, "is why we need type safety. TypedFetch can solve this." ## TypeScript + TypedFetch = Magic TypedFetch doesn't just fetch data - it understands it: ```typescript // Define your types interface User { id: number name: string email: string avatar?: string } // TypedFetch knows the type! const { data } = await tf.get('/api/users/123') console.log(data.name) // ✅ TypeScript knows this exists console.log(data.age) // ❌ Error: Property 'age' does not exist // Even better - runtime validation const { data, validated } = await tf.get('/api/users/123', { validate: true }) if (!validated) { console.error('API returned unexpected shape!') } ``` ## Runtime Type Inference: The Revolutionary Feature But here's where TypedFetch gets magical - it can learn types from actual API responses: ```typescript // First request - TypedFetch learns the shape const user1 = await tf.get('/api/users/1') // Second request - TypedFetch provides IntelliSense! const user2 = await tf.get('/api/users/2') // TypeScript now knows: user2.name, user2.email, etc. // Check what TypedFetch learned const typeInfo = tf.getTypeInfo('/api/users/*') console.log(typeInfo) // { // confidence: 0.95, // samples: 2, // schema: { // type: 'object', // properties: { // id: { type: 'number' }, // name: { type: 'string' }, // email: { type: 'string', format: 'email' } // } // } // } ``` ## OpenAPI Auto-Discovery: Types Without Writing Types TypedFetch can find and use OpenAPI schemas automatically: ```typescript // TypedFetch discovers OpenAPI spec await tf.discover('https://api.example.com') // Now EVERY endpoint has types! const users = await tf.get('/users') // ✅ Typed const posts = await tf.get('/posts') // ✅ Typed const comments = await tf.get('/comments') // ✅ Typed // See all discovered types const types = tf.getAllTypes() console.log(types) // { // '/users': '{ id: number, name: string, ... }', // '/posts': '{ id: number, title: string, ... }', // ... // } ``` ## Three Levels of Type Safety ### Level 1: Manual Types (Good) Define types yourself: ```typescript interface WeatherData { current_condition: [{ temp_C: string temp_F: string weatherDesc: [{ value: string }] humidity: string windspeedKmph: string }] nearest_area: [{ areaName: [{ value: string }] country: [{ value: string }] }] } const { data } = await tf.get(`https://wttr.in/${city}?format=j1`) // Full IntelliSense! ``` ### Level 2: Runtime Learning (Better) Let TypedFetch learn from responses: ```typescript // Enable type learning tf.configure({ inference: { enabled: true, minSamples: 3, // Need 3 samples before confident persistence: true // Save learned types } }) // First few calls - TypedFetch learns await tf.get('/api/products/1') await tf.get('/api/products/2') await tf.get('/api/products/3') // Now TypedFetch knows the type! const product = await tf.get('/api/products/4') // IntelliSense works without manual types! ``` ### Level 3: OpenAPI Integration (Best) Automatic type discovery: ```typescript // Option 1: Explicit discovery await tf.discover('https://api.example.com/openapi.json') // Option 2: Auto-discovery tf.configure({ autoDiscover: true // Looks for OpenAPI at common paths }) // Types everywhere! const result = await tf.get('/any/endpoint') // Fully typed based on OpenAPI spec ``` ## Type Validation: Trust but Verify Runtime validation catches API changes: ```typescript interface User { id: number name: string email: string role: 'admin' | 'user' } // Strict validation const { data, valid, errors } = await tf.get('/api/user', { validate: { strict: true, // Reject extra properties coerce: true, // Try to convert types throwOnError: false // Return errors instead of throwing } }) if (!valid) { console.error('Validation errors:', errors) // [ // { path: 'role', expected: 'admin|user', actual: 'superuser' }, // { path: 'age', message: 'Unexpected property' } // ] } // Custom validators const { data } = await tf.get('/api/user', { validate: { custom: (data) => { if (!data.email.includes('@')) { throw new Error('Invalid email format') } if (data.age && data.age < 0) { throw new Error('Age cannot be negative') } } } }) ``` ## Weather Buddy 7.0: Fully Typed Let's add complete type safety to Weather Buddy: ```typescript // types.ts export interface WeatherResponse { current_condition: CurrentCondition[] nearest_area: NearestArea[] request: Request[] weather: Weather[] } export interface CurrentCondition { FeelsLikeC: string FeelsLikeF: string cloudcover: string humidity: string localObsDateTime: string observation_time: string precipInches: string precipMM: string pressure: string pressureInches: string temp_C: string temp_F: string uvIndex: string visibility: string visibilityMiles: string weatherCode: string weatherDesc: WeatherDescription[] weatherIconUrl: WeatherIcon[] winddir16Point: string winddirDegree: string windspeedKmph: string windspeedMiles: string } export interface WeatherDescription { value: string } export interface WeatherIcon { value: string } export interface NearestArea { areaName: ValueWrapper[] country: ValueWrapper[] latitude: string longitude: string population: string region: ValueWrapper[] weatherUrl: ValueWrapper[] } export interface ValueWrapper { value: string } // weather-buddy-7.ts import { tf } from 'typedfetch' import type { WeatherResponse, CurrentCondition } from './types' // Configure TypedFetch with type inference tf.configure({ inference: { enabled: true, persistence: localStorage, minSamples: 2 }, validation: { enabled: true, strict: false } }) // Type-safe weather fetching async function getWeather(city: string): Promise<{ data: WeatherResponse cached: boolean inferred: boolean }> { const { data, cached, metadata } = await tf.get( `https://wttr.in/${city}?format=j1`, { validate: true, returnMetadata: true } ) return { data, cached, inferred: metadata.typeSource === 'inference' } } // Type-safe weather card component class WeatherCard { constructor(private city: string, private element: HTMLElement) {} async update(): Promise { try { const { data, cached, inferred } = await getWeather(this.city) // TypeScript knows all these properties! const current = data.current_condition[0] const area = data.nearest_area[0] this.render({ city: area.areaName[0].value, country: area.country[0].value, temperature: { celsius: parseInt(current.temp_C), fahrenheit: parseInt(current.temp_F) }, condition: current.weatherDesc[0].value, humidity: parseInt(current.humidity), wind: { speed: parseInt(current.windspeedKmph), direction: current.winddir16Point }, uv: parseInt(current.uvIndex), feelsLike: { celsius: parseInt(current.FeelsLikeC), fahrenheit: parseInt(current.FeelsLikeF) }, cached, inferred }) } catch (error) { this.renderError(error) } } private render(data: WeatherCardData): void { this.element.innerHTML = `
${data.cached ? '⚡ Cached' : '🌐 Fresh'} ${data.inferred ? '🧠 Inferred' : '📋 Typed'}

${data.city}, ${data.country}

${data.temperature.celsius}°C ${data.temperature.fahrenheit}°F

${data.condition}

💧 ${data.humidity}%
💨 ${data.wind.speed} km/h ${data.wind.direction}
☀️ UV ${data.uv}
🤔 Feels like ${data.feelsLike.celsius}°C
` } private renderError(error: unknown): void { if (error instanceof ValidationError) { this.element.innerHTML = `

Invalid API Response

The weather API returned unexpected data:

    ${error.errors.map(e => `
  • ${e.path}: ${e.message}
  • `).join('')}
` } else { this.element.innerHTML = `
${error.message}
` } } } interface WeatherCardData { city: string country: string temperature: { celsius: number fahrenheit: number } condition: string humidity: number wind: { speed: number direction: string } uv: number feelsLike: { celsius: number fahrenheit: number } cached: boolean inferred: boolean } // Auto-generate types from API async function exploreAPI(): Promise { console.log('🔍 Exploring API endpoints...') // Make a few requests to learn types const cities = ['London', 'Tokyo', 'New York'] for (const city of cities) { await getWeather(city) } // Check what TypedFetch learned const learned = tf.getTypeInfo('https://wttr.in/*') console.log('📚 Learned type schema:', learned) // Export for other developers const typescript = tf.exportTypes('https://wttr.in/*') console.log('📝 TypeScript definitions:', typescript) } // Type-safe configuration interface AppConfig { defaultCity: string units: 'metric' | 'imperial' refreshInterval: number maxCities: number } class TypedWeatherApp { private config: AppConfig private cards: Map = new Map() constructor(config: Partial = {}) { this.config = { defaultCity: 'London', units: 'metric', refreshInterval: 300000, // 5 minutes maxCities: 10, ...config } } async addCity(city: string): Promise { if (this.cards.size >= this.config.maxCities) { throw new Error(`Maximum ${this.config.maxCities} cities allowed`) } const element = document.createElement('div') const card = new WeatherCard(city, element) this.cards.set(city, card) document.getElementById('cities')?.appendChild(element) await card.update() } startAutoRefresh(): void { setInterval(() => { this.cards.forEach(card => card.update()) }, this.config.refreshInterval) } } // Usage with full type safety const app = new TypedWeatherApp({ defaultCity: 'San Francisco', units: 'metric', refreshInterval: 60000 }) // This would error at compile time: // app.addCity(123) // ❌ Argument of type 'number' is not assignable // app.config.units = 'kelvin' // ❌ Type '"kelvin"' is not assignable ``` ## Advanced Type Patterns ### 1. Discriminated Unions for API Responses Handle different response shapes safely: ```typescript // API can return different shapes based on status type ApiResponse = | { status: 'success'; data: T } | { status: 'error'; error: string; code: number } | { status: 'loading' } async function fetchData(url: string): Promise> { try { const { data } = await tf.get(url) return { status: 'success', data } } catch (error) { return { status: 'error', error: error.message, code: error.response?.status || 0 } } } // Type-safe handling const response = await fetchData('/api/user') switch (response.status) { case 'success': console.log(response.data.name) // ✅ TypeScript knows data exists break case 'error': console.log(response.error) // ✅ TypeScript knows error exists break case 'loading': // Handle loading state break } ``` ### 2. Type Guards for Runtime Validation ```typescript // Type guard functions function isUser(obj: unknown): obj is User { return ( typeof obj === 'object' && obj !== null && 'id' in obj && 'name' in obj && 'email' in obj && typeof (obj as any).id === 'number' && typeof (obj as any).name === 'string' && typeof (obj as any).email === 'string' ) } // Use with TypedFetch const response = await tf.get('/api/user') if (isUser(response.data)) { // TypeScript knows it's a User console.log(response.data.email) } else { console.error('Invalid user data received') } // Array type guard function isUserArray(obj: unknown): obj is User[] { return Array.isArray(obj) && obj.every(isUser) } ``` ### 3. Generic API Client Build type-safe API clients: ```typescript class TypedAPIClient> { constructor( private baseURL: string, private endpoints: TEndpoints ) {} async get( endpoint: K, params?: Record ): Promise { const { data } = await tf.get( `${this.baseURL}${String(endpoint)}`, { params } ) return data } } // Define your API interface MyAPI { '/users': User[] '/users/:id': User '/posts': Post[] '/posts/:id': Post '/comments': Comment[] } // Create typed client const api = new TypedAPIClient('https://api.example.com', { '/users': [] as User[], '/users/:id': {} as User, '/posts': [] as Post[], '/posts/:id': {} as Post, '/comments': [] as Comment[] }) // Full type safety! const users = await api.get('/users') // users: User[] const user = await api.get('/users/:id') // user: User // const invalid = await api.get('/invalid') // ❌ Error! ``` ### 4. Type Transformation Transform API responses to match your app's types: ```typescript // API returns snake_case interface APIUser { user_id: number first_name: string last_name: string email_address: string created_at: string } // Your app uses camelCase interface User { userId: number firstName: string lastName: string email: string createdAt: Date } // Type-safe transformer function transformUser(apiUser: APIUser): User { return { userId: apiUser.user_id, firstName: apiUser.first_name, lastName: apiUser.last_name, email: apiUser.email_address, createdAt: new Date(apiUser.created_at) } } // Use with TypedFetch interceptor tf.addResponseInterceptor(response => { if (response.config.url?.includes('/users')) { if (Array.isArray(response.data)) { response.data = response.data.map(transformUser) } else { response.data = transformUser(response.data) } } return response }) ``` ## Type Inference Deep Dive How TypedFetch learns types: ```typescript // Enable detailed inference tf.configure({ inference: { enabled: true, strategy: 'progressive', // Learn incrementally confidence: 0.9, // 90% confidence threshold maxSamples: 10, // Learn from up to 10 responses persistence: true, // Save learned types // Advanced options detectPatterns: true, // Detect email, URL, date formats detectEnums: true, // Detect enum-like fields detectOptional: true, // Detect optional fields mergeStrategy: 'union' // How to handle conflicts } }) // Watch TypedFetch learn tf.on('typeInferred', ({ endpoint, schema, confidence }) => { console.log(`Learned type for ${endpoint}:`, schema) console.log(`Confidence: ${confidence * 100}%`) }) // Make requests - TypedFetch learns await tf.get('/api/products/1') // Learns: { id, name, price } await tf.get('/api/products/2') // Confirms pattern await tf.get('/api/products/3') // High confidence now! // Check inference details const inference = tf.getInferenceDetails('/api/products/:id') console.log(inference) // { // samples: 3, // confidence: 0.95, // schema: { ... }, // patterns: { // id: 'number:integer', // price: 'number:currency', // email: 'string:email', // created: 'string:iso8601' // }, // optional: ['description'], // enums: { // status: ['active', 'inactive', 'pending'] // } // } ``` ## Generating Types from APIs TypedFetch can generate TypeScript definitions: ```typescript // Method 1: From OpenAPI const types = await tf.generateTypes({ source: 'https://api.example.com/openapi.json', output: './src/types/api.ts', options: { useUnknownForAny: true, generateEnums: true, addJSDoc: true } }) // Method 2: From learned types await tf.exportInferredTypes({ output: './src/types/inferred.ts', filter: (endpoint) => endpoint.startsWith('/api/v2'), options: { includeConfidence: true, minConfidence: 0.8 } }) // Method 3: From live exploration const explorer = tf.createExplorer() await explorer.explore('https://api.example.com', { depth: 3, // Follow links 3 levels deep samples: 5 // Try 5 examples of each endpoint }) await explorer.generateTypes('./src/types/explored.ts') ``` ## Best Practices for Type Safety 🎯 ### 1. Start with Strict Types ```typescript // tsconfig.json { "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true, "noUncheckedIndexedAccess": true } } ``` ### 2. Validate at Boundaries ```typescript // Always validate external data async function getUser(id: string): Promise { const { data } = await tf.get(`/api/users/${id}`) if (!isUser(data)) { throw new Error('Invalid user data from API') } return data } ``` ### 3. Use Branded Types ```typescript // Prevent mixing up similar types type UserId = string & { readonly brand: unique symbol } type PostId = string & { readonly brand: unique symbol } function getUserById(id: UserId) { /* ... */ } function getPostById(id: PostId) { /* ... */ } const userId = '123' as UserId const postId = '456' as PostId getUserById(userId) // ✅ OK getUserById(postId) // ❌ Error! ``` ### 4. Prefer Unknown to Any ```typescript // ❌ Bad: any disables all checking async function processData(data: any) { console.log(data.foo.bar.baz) // No errors, but crashes at runtime } // ✅ Good: unknown requires checking async function processData(data: unknown) { if (typeof data === 'object' && data !== null && 'foo' in data) { // Safe to use data.foo } } ``` ## Practice Time! 🏋️ ### Exercise 1: Type-Safe API Wrapper Create a fully typed API wrapper: ```typescript class TypedAPI { // Your code here: // - Generic endpoints // - Type validation // - Transform responses // - Handle errors with types } ``` ### Exercise 2: Runtime Type Validator Build a runtime type validation system: ```typescript class TypeValidator { // Your code here: // - Define schema // - Validate at runtime // - Produce typed results // - Helpful error messages } ``` ### Exercise 3: Type Learning System Implement type inference from responses: ```typescript class TypeLearner { // Your code here: // - Analyze responses // - Build schemas // - Track confidence // - Export TypeScript } ``` ## Key Takeaways 🎯 1. **TypeScript prevents errors at compile time** - Catch bugs before running 2. **Runtime validation catches API changes** - Trust but verify 3. **TypedFetch can infer types automatically** - Learn from responses 4. **OpenAPI integration provides instant types** - No manual definitions 5. **Type guards ensure runtime safety** - Validate at boundaries 6. **Generic patterns enable reusable code** - Write once, type everywhere 7. **Transform types at the edge** - Keep internals clean ## Common Pitfalls 🚨 1. **Trusting API types blindly** - Always validate 2. **Using 'any' to silence errors** - Use 'unknown' instead 3. **Not handling optional fields** - Check for undefined 4. **Ignoring runtime validation** - TypeScript can't catch everything 5. **Over-typing internal code** - Type at boundaries 6. **Fighting type inference** - Let TypeScript help ## What's Next? You've achieved type safety nirvana! But how do you modify requests and responses in flight? In Chapter 8, we'll explore interceptors and middleware: - Request/response transformation pipelines - Authentication interceptors - Logging and analytics - Request signing - Response normalization - Building plugin systems Ready to intercept and transform? See you in Chapter 8! 🚦 --- ## Chapter Summary - TypeScript + TypedFetch provides compile-time and runtime type safety - Manual types are good, runtime inference is better, OpenAPI is best - TypedFetch learns types from actual API responses automatically - Validation at runtime catches API changes before they break your app - Type guards and discriminated unions handle complex response shapes - Generic patterns enable fully typed, reusable API clients - Always validate external data at system boundaries - Weather Buddy 7.0 shows type sources and validates all responses **Next Chapter Preview**: Interceptors & Middleware - Transform requests and responses, add authentication, log everything, and build powerful plugin systems.