Summary
When a POST/PUT/PATCH/DELETE endpoint's validator has an open-record inferred input (e.g. vine.record(vine.string()) from VineJS, whose InferInput is { [K: string]: string }), the generated registry emits:
query: ExtractQuery<InferInput<typeofcontactValidator>>
ExtractQuery resolves to unknown, which violates EndpointTypes['query']: Record<string, any>. tsc then fails inside the generated registry/index.ts (satisfies Record<string, AdonisEndpoint>) and at the createTuyau({ registry }) call site.
Environment
@tuyau/core: 1.2.2@vinejs/vine: 4.4.0- TypeScript: 5.9
- AdonisJS 7 app using
generateRegistry() from @tuyau/core/hooks
Minimal reproduction
// app/validators/contact.tsimportvinefrom'@vinejs/vine'exportconstcontactValidator=vine.create(vine.record(vine.string().trim().maxLength(2000)))
// controllerexportdefaultclassContactController{asyncexecute({ request, response }: HttpContext){constpayload=awaitcontactValidator.validate(request.all())// ...returnresponse.redirect().back()}}router.post('/contact',[ContactController,'execute'])// clientimport{registry}from'./.adonisjs/client/registry'import{createTuyau}from'@tuyau/core/client'exportconstclient=createTuyau({baseUrl: '/', registry })Actual behavior
tsc --noEmit fails with:
.adonisjs/client/registry/index.ts: error TS2322: Type '{ body: ...; query: unknown; ... }' is not assignable to type 'EndpointTypes'.
Types of property 'query' are incompatible.
Type 'unknown' is not assignable to type 'Record<string, any>'.
and the same incompatibility cascades into the createTuyau({ registry }) call site (TuyauRegistry constraint).
Root cause
ExtractQuery is defined as:
typeExtractQuery<T>='query'extendskeyofT ? Textends{query?: infer Q} ? Q : {} : {}For T = { [K: string]: string }:
'query' extends keyof T is true (keyof of a string index signature is string | number), so the guard does not filter open records out.- TypeScript does not infer from an index signature into an optional property position:
T extends { query?: infer Q } matches, but there is no declaredquery property, so Q has no inference candidates and falls back to unknown.
Hence query: unknown in the generated registry, which is not assignable to Record<string, any>.
Note that ExtractBody (DistributiveOmit) handles the same input fine — only query breaks.
Expected behavior
An open-record validator declares no query params, so query should resolve to {} instead of unknown.
A minimal, semantics-preserving fix in ExtractQuery — only accept the inferred type when it satisfies the Record<string, any> constraint that EndpointTypes requires, otherwise fall back to {}:
typeExtractQuery<T>='query'extendskeyofT
? Textends{query?: infer Q}
? [Q]extends[Record<string,any>]
? Q
: {}
: {}
: {}This only changes outcomes that are currently hard type errors (unknown, or a scalar query field as in #115); object-typed query declarations keep working exactly as today.
Related
Summary
When a POST/PUT/PATCH/DELETE endpoint's validator has an open-record inferred input (e.g.
vine.record(vine.string())from VineJS, whoseInferInputis{ [K: string]: string }), the generated registry emits:ExtractQueryresolves tounknown, which violatesEndpointTypes['query']: Record<string, any>.tscthen fails inside the generatedregistry/index.ts(satisfies Record<string, AdonisEndpoint>) and at thecreateTuyau({ registry })call site.Environment
@tuyau/core: 1.2.2@vinejs/vine: 4.4.0generateRegistry()from@tuyau/core/hooksMinimal reproduction
Actual behavior
tsc --noEmitfails with:and the same incompatibility cascades into the
createTuyau({ registry })call site (TuyauRegistryconstraint).Root cause
ExtractQueryis defined as:For
T = { [K: string]: string }:'query' extends keyof Tis true (keyofof a string index signature isstring | number), so the guard does not filter open records out.T extends { query?: infer Q }matches, but there is no declaredqueryproperty, soQhas no inference candidates and falls back tounknown.Hence
query: unknownin the generated registry, which is not assignable toRecord<string, any>.Note that
ExtractBody(DistributiveOmit) handles the same input fine — onlyquerybreaks.Expected behavior
An open-record validator declares no query params, so
queryshould resolve to{}instead ofunknown.A minimal, semantics-preserving fix in
ExtractQuery— only accept the inferred type when it satisfies theRecord<string, any>constraint thatEndpointTypesrequires, otherwise fall back to{}:This only changes outcomes that are currently hard type errors (
unknown, or a scalarqueryfield as in #115); object-typedquerydeclarations keep working exactly as today.Related
query. This issue is the open-record variant: no field namedqueryexists at all, yet the index signature still trips the guard.