TypeScript Integration
TypeScript Integration
@shkumbinhsn/fetcher is built with TypeScript-first design. This guide covers advanced TypeScript patterns and how to get the most out of the library’s type system.
Type Inference
The library automatically infers response types from your schemas:
import { fetcher } from '@shkumbinhsn/fetcher';import { z } from 'zod';
const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email()});
// user is automatically typed as:// { id: string; name: string; email: string }const user = await fetcher('/api/users/123', { schema: UserSchema});
// TypeScript provides full IntelliSenseuser.name; // ✓ stringuser.email; // ✓ stringuser.age; // ✗ Property 'age' does not existCustom Request Types
Extend the request initialization type for your specific use cases:
import type { FetcherRequestInit } from '@shkumbinhsn/fetcher';
// Create a custom interface that extends FetcherRequestInitinterface ApiRequestInit<T> extends FetcherRequestInit<T> { // Add custom properties retries?: number; timeout?: number; cache?: boolean;}
// Create a wrapper function with your custom typeasync function apiCall<TSchema>( url: string, init?: ApiRequestInit<TSchema>) { const { retries, timeout, cache, ...fetchInit } = init || {};
// Handle custom logic here if (timeout) { const controller = new AbortController(); setTimeout(() => controller.abort(), timeout); fetchInit.signal = controller.signal; }
return fetcher(url, fetchInit);}
// Usage with full type safetyconst user = await apiCall('/api/users/123', { schema: UserSchema, retries: 3, timeout: 5000});Type-Safe API Client
Create a fully typed API client:
import { fetcher, defineError, type FetcherRequestInit } from '@shkumbinhsn/fetcher';import { z } from 'zod';
// Define your schemasconst UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email()});
const CreateUserSchema = z.object({ name: z.string(), email: z.string().email()});
// Define errorsconst NotFoundError = defineError(404, z.object({ message: z.string(), resource: z.string()}));
const ValidationError = defineError(400, z.object({ errors: z.array(z.object({ field: z.string(), message: z.string() }))}));
// Create API client classclass ApiClient { private baseUrl: string; private commonHeaders: Record<string, string>;
constructor(baseUrl: string, token?: string) { this.baseUrl = baseUrl; this.commonHeaders = { 'Content-Type': 'application/json', ...(token && { 'Authorization': `Bearer ${token}` }) }; }
private async request<T>( endpoint: string, init?: FetcherRequestInit<T> ) { return fetcher(`${this.baseUrl}${endpoint}`, { headers: { ...this.commonHeaders, ...init?.headers }, errors: [NotFoundError, ValidationError], ...init }); }
// Typed API methods async getUser(id: string) { return this.request(`/users/${id}`, { schema: UserSchema }); }
async getUsers() { return this.request('/users', { schema: z.array(UserSchema) }); }
async createUser(data: z.infer<typeof CreateUserSchema>) { return this.request('/users', { method: 'POST', body: JSON.stringify(data), schema: UserSchema }); }
async updateUser(id: string, data: Partial<z.infer<typeof UserSchema>>) { return this.request(`/users/${id}`, { method: 'PATCH', body: JSON.stringify(data), schema: UserSchema }); }
async deleteUser(id: string) { return this.request(`/users/${id}`, { method: 'DELETE' }); }}
// Usageconst client = new ApiClient('https://api.example.com', 'your-token');
const user = await client.getUser('123'); // Type: Userconst users = await client.getUsers(); // Type: User[]const newUser = await client.createUser({ // Type-safe input name: 'John', email: 'john@example.com'});Generic Response Types
Handle generic API response patterns:
// Common API response wrapperconst ApiResponseSchema = <T extends z.ZodType>(dataSchema: T) => z.object({ success: z.boolean(), data: dataSchema, message: z.string().optional(), pagination: z.object({ page: z.number(), limit: z.number(), total: z.number(), pages: z.number() }).optional() });
// Usageconst usersResponse = await fetcher('/api/users', { schema: ApiResponseSchema(z.array(UserSchema))});
if (usersResponse.success) { usersResponse.data.forEach(user => { console.log(user.name); // Fully typed });
if (usersResponse.pagination) { console.log(`Page ${usersResponse.pagination.page} of ${usersResponse.pagination.pages}`); }}Conditional Types
Use conditional types for different response formats:
type ApiResponse<T> = T extends undefined ? any : T extends z.ZodType ? z.infer<T> : never;
function createApiCall<TSchema extends z.ZodType | undefined = undefined>( defaultSchema?: TSchema) { return async function<TOverrideSchema extends z.ZodType | undefined = TSchema>( url: string, init?: FetcherRequestInit<TOverrideSchema> ): Promise<ApiResponse<TOverrideSchema extends undefined ? TSchema : TOverrideSchema>> { return fetcher(url, { schema: init?.schema ?? defaultSchema, ...init }) as any; };}
// Create typed API functionsconst getUser = createApiCall(UserSchema);const getAny = createApiCall();
const user = await getUser('/api/users/123'); // Type: Userconst anything = await getAny('/api/anything'); // Type: anyUtility Types
Create utility types for common patterns:
import type { InferResponse } from '@shkumbinhsn/fetcher';
// Extract the response type from a schematype UserType = InferResponse<typeof UserSchema>;
// Create a type for API endpointstype ApiEndpoint<TSchema extends z.ZodType> = { schema: TSchema; path: string; method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';};
// Define your API endpoints with typesconst endpoints = { getUser: { schema: UserSchema, path: '/users/:id', method: 'GET' } as const, createUser: { schema: UserSchema, path: '/users', method: 'POST' } as const} as const;
// Type-safe endpoint callerasync function callEndpoint<K extends keyof typeof endpoints>( key: K, pathParams: Record<string, string> = {}, init?: Omit<FetcherRequestInit<typeof endpoints[K]['schema']>, 'schema'>) { const endpoint = endpoints[key]; let path = endpoint.path;
// Replace path parameters Object.entries(pathParams).forEach(([param, value]) => { path = path.replace(`:${param}`, value); });
return fetcher(path, { method: endpoint.method || 'GET', schema: endpoint.schema, ...init });}
// Usageconst user = await callEndpoint('getUser', { id: '123' });Discriminated Unions
Handle different response shapes based on status:
const SuccessResponse = z.object({ status: z.literal('success'), data: UserSchema});
const ErrorResponse = z.object({ status: z.literal('error'), error: z.object({ code: z.string(), message: z.string() })});
const ResponseSchema = z.union([SuccessResponse, ErrorResponse]);
const response = await fetcher('/api/users/123', { schema: ResponseSchema});
// TypeScript narrows the type based on discriminantif (response.status === 'success') { console.log(response.data.name); // TypeScript knows data exists} else { console.log(response.error.message); // TypeScript knows error exists}Higher-Order Functions
Create higher-order functions for common patterns:
function withRetry<T extends any[], R>( fn: (...args: T) => Promise<R>, maxRetries: number = 3) { return async (...args: T): Promise<R> => { let lastError: unknown;
for (let i = 0; i <= maxRetries; i++) { try { return await fn(...args); } catch (error) { lastError = error; if (i === maxRetries) break;
// Exponential backoff await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000)); } }
throw lastError; };}
// Usageconst getUserWithRetry = withRetry(async (id: string) => { return fetcher(`/api/users/${id}`, { schema: UserSchema, errors: [NotFoundError] });});
const user = await getUserWithRetry('123'); // Type: UserAdvanced Error Typing
Create strongly typed error handling:
type ApiError = | InstanceType<typeof NotFoundError> | InstanceType<typeof ValidationError> | InstanceType<typeof UnauthorizedError>;
function isApiError(error: unknown): error is ApiError { return error instanceof NotFoundError || error instanceof ValidationError || error instanceof UnauthorizedError;}
async function safeApiCall<T>( apiCall: () => Promise<T>): Promise<{ data: T } | { error: ApiError }> { try { const data = await apiCall(); return { data }; } catch (error) { if (isApiError(error)) { return { error }; } throw error; // Re-throw non-API errors }}
// Usageconst result = await safeApiCall(() => fetcher('/api/users/123', { schema: UserSchema, errors: [NotFoundError, ValidationError] }));
if ('data' in result) { console.log(result.data.name); // Type: string} else { // Type-safe error handling if (result.error instanceof NotFoundError) { console.log('User not found'); }}Next Steps
- React Query Integration - Use with React Query for data fetching
- API Reference - Complete type definitions
- Examples - Advanced usage patterns