Skip to main content
Search...

types

All types exported by Duck Query. Use them to build custom route maps, extract type information, and integrate with your own tooling.

Overview

Duck Query exports several types that power its type-safe client. You can also use these types independently to:

  • Build custom route maps without Duck Gen.
  • Extract type information from existing route maps.
  • Build your own utilities on top of the type system.
import type {
  DuckRouteMeta,
  DuckApiRoutes,
  DuckQueryClient,
  RoutePath,
  RouteOf,
  RouteMethod,
  RouteRes,
  RouteReq,
  RouteMethods,
  RouteOfMethod,
  RouteResMethod,
  RouteReqMethod,
  PathsByMethod,
} from '@gentleduck/query'

Core types

DuckRouteMeta

The shape of a single route's metadata. Every route in a route map must conform to this:

type DuckRouteMeta = {
  body: unknown
  query: unknown
  params: unknown
  headers: unknown
  res: unknown
  method: string
}

Use never for fields that don't apply to a route:

type PingRoute = {
  body: never      // no body for GET
  query: never     // no query params
  params: never    // no path params
  headers: never   // no special headers
  res: { ok: true }
  method: 'GET'
}

DuckApiRoutes

A type alias for a route map, a Record mapping path strings to DuckRouteMeta:

type DuckApiRoutes = Record<string, DuckRouteMeta>

Use it as a constraint when building generic functions:

function createLogger<R extends DuckApiRoutes>(client: DuckQueryClient<R>) {
  // ...
}

DuckQueryClient

The return type of createDuckQueryClient. Contains all client methods:

type DuckQueryClient<Routes> = {
  axios: AxiosInstance
  request: (path, req?, config?) => Promise<AxiosResponse>
  byMethod: (method, path, req?, config?) => Promise<AxiosResponse>
  get: (path, req?, config?) => Promise<AxiosResponse>
  post: (path, req, config?) => Promise<AxiosResponse>
  put: (path, req, config?) => Promise<AxiosResponse>
  patch: (path, req, config?) => Promise<AxiosResponse>
  del: (path, req?, config?) => Promise<AxiosResponse>
}

Route extractors

These types extract specific pieces of information from a route map.

RoutePath

Extracts all path strings from a route map as a union:

type RoutePath<Routes> = keyof Routes & string
type MyRoutes = {
  '/users': { ... }
  '/users/:id': { ... }
  '/auth/signin': { ... }
}
 
type Paths = RoutePath<MyRoutes>
// => '/users' | '/users/:id' | '/auth/signin'

RouteOf

Gets the full metadata for a specific path:

type RouteOf<Routes, P extends RoutePath<Routes>> = Routes[P]
type UserRoute = RouteOf<MyRoutes, '/users/:id'>
// => { body: never; query: never; params: { id: string }; ... }

RouteMethod

Extracts the HTTP method for a path:

type RouteMethod<Routes, P extends RoutePath<Routes>> = RouteOf<Routes, P>['method']
type Method = RouteMethod<MyRoutes, '/auth/signin'>
// => 'POST'

RouteRes

Extracts the response type for a path:

type RouteRes<Routes, P extends RoutePath<Routes>> = RouteOf<Routes, P>['res']
type UserResponse = RouteRes<MyRoutes, '/users/:id'>
// => { id: string; name: string }

RouteReq

Extracts the request shape for a path, with never fields removed:

type RouteReq<Routes, P extends RoutePath<Routes>> = CleanupNever<
  Pick<RouteOf<Routes, P>, 'body' | 'query' | 'params' | 'headers'>
>

The internal CleanupNever utility removes fields whose value is never, so you only see the fields that actually apply:

type UserReq = RouteReq<MyRoutes, '/users/:id'>
// => { params: { id: string } }
// (body, query, headers are never, so they are removed)

RouteMethods

A union of all HTTP methods used across all routes:

type RouteMethods<Routes> = RouteOf<Routes, RoutePath<Routes>>['method']
type AllMethods = RouteMethods<MyRoutes>
// => 'GET' | 'POST'

PathsByMethod

Filters paths to only those supporting a given HTTP method:

type PathsByMethod<Routes, M extends string> = {
  [P in RoutePath<Routes>]: M extends RouteMethod<Routes, P> ? P : never
}[RoutePath<Routes>]
type GetPaths = PathsByMethod<MyRoutes, 'GET'>
// => '/users' | '/users/:id'
 
type PostPaths = PathsByMethod<MyRoutes, 'POST'>
// => '/auth/signin'

This is what makes client.get() and client.post() only accept paths that support the corresponding method.

Method-specific extractors

These types combine path and method filtering for precise type extraction.

RouteOfMethod

Gets route metadata for a path filtered by method. Useful when a path supports multiple methods:

type RouteOfMethod<Routes, P, M extends string> = Extract<RouteOf<Routes, P>, { method: M }>
type MyRoutes = {
  '/users/:id': {
    body: never; query: never; params: { id: string }; headers: never;
    res: UserDto; method: 'GET'
  } | {
    body: UpdateUserDto; query: never; params: { id: string }; headers: never;
    res: UserDto; method: 'PUT'
  }
}
 
type GetUser = RouteOfMethod<MyRoutes, '/users/:id', 'GET'>
// => { body: never; ...; res: UserDto; method: 'GET' }
 
type UpdateUser = RouteOfMethod<MyRoutes, '/users/:id', 'PUT'>
// => { body: UpdateUserDto; ...; res: UserDto; method: 'PUT' }

RouteResMethod

Response type for a path and method combination:

type RouteResMethod<Routes, P, M extends string> = RouteOfMethod<Routes, P, M>['res']

RouteReqMethod

Request shape for a path and method combination:

type RouteReqMethod<Routes, P, M extends string> = CleanupNever<
  Pick<RouteOfMethod<Routes, P, M>, 'body' | 'query' | 'params' | 'headers'>
>

Building a custom route map

Here is a complete example of defining a route map without Duck Gen:

import { createDuckQueryClient } from '@gentleduck/query'
 
// Define your route map
type Routes = {
  '/health': {
    method: 'GET'
    params: never
    query: never
    headers: never
    body: never
    res: { status: 'ok'; uptime: number }
  }
  '/auth/login': {
    method: 'POST'
    params: never
    query: never
    headers: never
    body: { email: string; password: string }
    res: { token: string; expiresAt: string }
  }
  '/users/:id': {
    method: 'GET'
    params: { id: string }
    query: { include?: 'profile' | 'settings' }
    headers: { authorization: string }
    body: never
    res: { id: string; name: string; email: string }
  }
  '/users/:id': {
    method: 'PUT'
    params: { id: string }
    query: never
    headers: { authorization: string }
    body: { name?: string; email?: string }
    res: { id: string; name: string; email: string }
  }
}
 
// Create the client
const client = createDuckQueryClient<Routes>({
  baseURL: 'http://localhost:3000',
})
 
// All calls are type-safe
const { data: health } = await client.get('/health')
// health: { status: 'ok'; uptime: number }
 
const { data: session } = await client.post('/auth/login', {
  body: { email: 'duck@example.com', password: '123456' },
})
// session: { token: string; expiresAt: string }
 
const { data: user } = await client.get('/users/:id', {
  params: { id: 'u_123' },
  query: { include: 'profile' },
  headers: { authorization: `Bearer ${session.token}` },
})
// user: { id: string; name: string; email: string }

Next steps