Skip to main content

Overview

You can augment the FetchOptions interface to add custom properties with full TypeScript support. This is useful for adding domain-specific options that work throughout your application.

Basic Usage

Place this in any .ts or .d.ts file in your project:
// types.d.ts or custom.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    // Add your custom properties
    requiresAuth?: boolean
  }
}

export {}
Ensure the file is included in your tsconfig.json “files” or covered by “include” patterns.

Using Augmented Types

Once augmented, your custom properties are available everywhere:
import { ofetch } from 'ofetch'

// TypeScript knows about requiresAuth
await ofetch('/api/protected', {
  requiresAuth: true  // ✅ Type-safe
})

Example from README

From README.md:356-384:
// Place this in any `.ts` or `.d.ts` file.
// Ensure it's included in the project's tsconfig.json "files".
declare module "ofetch" {
  interface FetchOptions {
    // Custom properties
    requiresAuth?: boolean;
  }
}

export {};
This lets you pass and use those properties with full type safety throughout ofetch calls:
const myFetch = ofetch.create({
  onRequest(context) {
    //      ^? { ..., options: {..., requiresAuth?: boolean }}
    console.log(context.options.requiresAuth);
  },
});

myFetch("/foo", { requiresAuth: true });

Common Use Cases

Authentication Requirements

// types.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    requiresAuth?: boolean
    authType?: 'bearer' | 'basic' | 'apiKey'
  }
}

export {}
// api-client.ts
import { ofetch } from 'ofetch'

const api = ofetch.create({
  baseURL: '/api',
  async onRequest({ options }) {
    if (options.requiresAuth) {
      const token = await getAuthToken()
      
      if (options.authType === 'bearer') {
        options.headers.set('Authorization', `Bearer ${token}`)
      } else if (options.authType === 'apiKey') {
        options.headers.set('X-API-Key', token)
      }
    }
  }
})

// Usage with type safety
await api('/protected/resource', {
  requiresAuth: true,
  authType: 'bearer'
})

Cache Control

// types.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    cacheStrategy?: 'no-cache' | 'force-cache' | 'revalidate'
    cacheTTL?: number
  }
}

export {}
const api = ofetch.create({
  async onRequest({ options }) {
    if (options.cacheStrategy === 'no-cache') {
      options.headers.set('Cache-Control', 'no-cache')
    } else if (options.cacheStrategy === 'force-cache') {
      options.headers.set('Cache-Control', `max-age=${options.cacheTTL || 3600}`)
    }
  }
})

await api('/api/data', {
  cacheStrategy: 'force-cache',
  cacheTTL: 7200
})

Rate Limiting

// types.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    rateLimit?: {
      maxRequests: number
      perSeconds: number
    }
  }
}

export {}
const rateLimiters = new Map<string, RateLimiter>()

const api = ofetch.create({
  async onRequest({ request, options }) {
    if (options.rateLimit) {
      const limiter = getRateLimiter(
        request.toString(),
        options.rateLimit
      )
      await limiter.acquire()
    }
  }
})

await api('/api/endpoint', {
  rateLimit: { maxRequests: 10, perSeconds: 60 }
})

API Versioning

// types.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    apiVersion?: 'v1' | 'v2' | 'v3'
  }
}

export {}
const api = ofetch.create({
  baseURL: '/api',
  async onRequest({ options }) {
    const version = options.apiVersion || 'v2'
    options.headers.set('X-API-Version', version)
  }
})

await api('/users', { apiVersion: 'v3' })

Request Tracking

// types.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    trackingId?: string
    analytics?: boolean
  }
}

export {}
const api = ofetch.create({
  async onRequest({ options }) {
    if (options.analytics !== false) {
      const trackingId = options.trackingId || generateId()
      options.headers.set('X-Request-ID', trackingId)
      trackRequest(trackingId)
    }
  },
  async onResponse({ options, response }) {
    if (options.analytics !== false && options.trackingId) {
      trackResponse(options.trackingId, response.status)
    }
  }
})

await api('/api/action', {
  trackingId: 'user-action-123',
  analytics: true
})

Multiple Augmentations

You can augment the interface multiple times:
// auth.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    requiresAuth?: boolean
  }
}

export {}
// cache.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    cacheKey?: string
  }
}

export {}
Both properties will be available:
await ofetch('/api/data', {
  requiresAuth: true,
  cacheKey: 'my-data'
})

Context Types

From src/types.ts:94-99:
export interface FetchContext<T = any, R extends ResponseType = ResponseType> {
  request: FetchRequest;
  options: ResolvedFetchOptions<R>;
  response?: FetchResponse<T>;
  error?: Error;
}
Your augmented properties are available in interceptors through context.options:
declare module 'ofetch' {
  interface FetchOptions {
    customProperty?: string
  }
}

const api = ofetch.create({
  async onRequest(context) {
    // TypeScript knows about customProperty
    console.log(context.options.customProperty)
  }
})

Best Practices

1. Use Optional Properties

// ✅ Good - optional properties
declare module 'ofetch' {
  interface FetchOptions {
    myOption?: string
  }
}

// ❌ Avoid - required properties (breaks existing code)
declare module 'ofetch' {
  interface FetchOptions {
    myOption: string
  }
}

2. Document Your Properties

declare module 'ofetch' {
  interface FetchOptions {
    /**
     * Requires authentication for this request.
     * @default false
     */
    requiresAuth?: boolean
    
    /**
     * Custom timeout in milliseconds for this specific request.
     * Overrides the global timeout setting.
     */
    customTimeout?: number
  }
}

export {}

3. Keep Type Files Organized

// types/ofetch.d.ts
import 'ofetch'

declare module 'ofetch' {
  interface FetchOptions {
    // Group related properties
    
    // Authentication
    requiresAuth?: boolean
    authType?: 'bearer' | 'basic'
    
    // Caching
    cacheStrategy?: 'no-cache' | 'force-cache'
    cacheTTL?: number
    
    // Tracking
    trackingId?: string
    analytics?: boolean
  }
}

export {}

4. Export Empty Object

// ✅ Required - makes this a module
declare module 'ofetch' {
  interface FetchOptions {
    myOption?: string
  }
}

export {} // Don't forget this!

tsconfig.json Setup

Make sure your type declaration files are included:
{
  "compilerOptions": {
    "types": ["ofetch"]
  },
  "include": [
    "src/**/*",
    "types/**/*"  // Include your .d.ts files
  ]
}

Complete Example

// types/ofetch.d.ts
declare module 'ofetch' {
  interface FetchOptions {
    /** Require authentication for this request */
    requiresAuth?: boolean
    /** API version to use */
    apiVersion?: 'v1' | 'v2'
    /** Custom request ID for tracking */
    requestId?: string
  }
}

export {}
// api/client.ts
import { ofetch } from 'ofetch'

export const api = ofetch.create({
  baseURL: '/api',
  
  async onRequest({ options }) {
    // Add version header
    if (options.apiVersion) {
      options.headers.set('X-API-Version', options.apiVersion)
    }
    
    // Add auth token
    if (options.requiresAuth) {
      const token = await getAuthToken()
      options.headers.set('Authorization', `Bearer ${token}`)
    }
    
    // Add request ID
    const requestId = options.requestId || crypto.randomUUID()
    options.headers.set('X-Request-ID', requestId)
  }
})
// Usage with full type safety
const user = await api('/users/me', {
  requiresAuth: true,
  apiVersion: 'v2',
  requestId: 'custom-id-123'
})

Build docs developers (and LLMs) love