Skip to main content

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 the types that power its client. Use them on their own to:

  • Build custom route maps without Duck Gen.
  • Extract type information from an existing route map.
  • Build 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'
import type {
  DuckRouteMeta,
  DuckApiRoutes,
  DuckQueryClient,
  RoutePath,
  RouteOf,
  RouteMethod,
  RouteRes,
  RouteReq,
  RouteMethods,
  RouteOfMethod,
  RouteResMethod,
  RouteReqMethod,
  PathsByMethod,
} from '@gentleduck/query'

Core types

DuckRouteMeta

Shape of a single route's metadata. Every route must conform to this:

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

Use never for fields that don't apply:

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'
}
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

Route map alias — a Record of path strings to DuckRouteMeta:

type DuckApiRoutes = Record<string, DuckRouteMeta>
type DuckApiRoutes = Record<string, DuckRouteMeta>

Use it as a generic constraint:

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

DuckQueryClient

Return type of createDuckQueryClient — 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>
}
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 pull specific information out of a route map.

RoutePath

All path strings from a route map, as a union:

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

RouteOf

Full metadata for a given path:

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

RouteMethod

HTTP method for a path:

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

RouteRes

Response type for a path:

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

RouteReq

Request shape for a path, with never fields stripped:

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

The internal CleanupNever utility drops fields typed as never, so only the applied fields remain:

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

RouteMethods

Union of every HTTP method used across the route map:

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

PathsByMethod

Paths that support a given HTTP method:

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

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

Method-specific extractors

These combine path and method filtering for exact type extraction.

RouteOfMethod

Route metadata for a path filtered by method — handy when a path supports multiple methods:

type RouteOfMethod<Routes, P, M extends string> = Extract<RouteOf<Routes, P>, { method: M }>
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' }
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, method) pair:

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

RouteReqMethod

Request shape for a (path, method) pair:

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

Building a custom route map

A complete 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 }
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