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 & stringtype 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
- Client Methods: detailed method documentation.
- Advanced: interceptors, patterns, and error handling.