Closed climba03003 closed 1 month ago
@climba03003 Hi! Apologies for the delay in reply (Been very busy of late)
I knows that I can use Type.Intersect for the same effect. But using Type.Composite to generate single object schema provides better performance in fastify. For example the below querystring, the types is incomplete and missing the patternProperties in result.
The missing properties in commonQueryString
are largely a result of the following aspects.
TObject
or TRecord
type based on the Key type. TypeBox will return a TObject
if it can determine that the Key parameter yields a finite set (which maps to a finite set of properties). As the where.${string}
key is infinite in your case, it generates a TRecord
with patternProperties.Unfortunately, it's proven very difficult to integrate Record into Type.Composite compositions due to their nature (while having inference work correctly), which is also somewhat compounded by the options on TObject not contributing to inference results (which I plan to address in later revisions, but not quite sure the best way to proceed at this stage)
This said, the following should provide a reasonable workaround.
import { Type, Static, Evaluate } from '@sinclair/typebox'
export type CommonQueryString = Evaluate<Static<typeof CommonQueryString>
& Record<`where.${string}`, string>
& Record<`orderby.${string}`, string>>
export const CommonQueryString = Type.Object({
search: Type.Optional(Type.String()),
fields: Type.Optional(Type.String()),
page: Type.Optional(Type.Number({ minimum: 1 })),
pageSize: Type.Optional(Type.Number({ minimum: 1 })),
}, {
additionalProperties: false,
patternProperties: {
'^where.(.*)': Type.String(),
'^orderby.(.*)': Type.String(),
}
})
It would be easier to directly support patternProperties through Type.Object in some way. So, it can eliminate the usages of Type.Composite.
So, the above should do this. But just on Type.Composite. If you can avoid using this type (and preference using Intersect), it will be better in the long run. I am currently leaning towards the following future API as an alternative.
const A = Type.Object({ a: Type.Number() })
const B = Type.Object({ b: Type.Number() })
const I = Type.Intersect([A, B])
// Evaluate to replace Composite
const C = Type.Evaluate(I) // TObject<{ a: TNumber, b: TNumber }>
The introduction of the Evaluate function would operate similar to Composite, but is intended to emulate the Evaluate<T>
type (in the example given). Unfortunately the complexity of this type has proven extremely difficult to get right (so is deferred for now). The general recommendation though is to try and use Intersect over Composite if you can, with the expectation that TypeBox may be able to optimize your schematics with Evaluate in future.
Hope this helps and brings a bit of insight into things All the best S
Actually, the types will go through another helper utility.
So, it will use the actual const
value and evaluate again which eliminate the override by type
.
interface ExtractRouteGeneric<T extends Record<string, any>> {
Params: Required<Static<T['params']>>
Headers: Required<Static<T['headers']>>
Querystring: Required<Static<T['querystring']>>
Body: Required<Static<T['body']>>
}
Currently, I want to prevent Type.Intersect
basically because of allOf
.
For now, I just replace the generic with what I expect.
import { Type, Static, TOptional, TString, TNumber } from '@sinclair/typebox'
export type CommonQueryString = Static<typeof CommonQueryString>
export const CommonQueryString = Type.Object<{
search: TOptional<TString>
fields: TOptional<TString>
page: TOptional<TNumber>
pageSize: TOptional<TNumber>,
// patternProperties
[key: `where.${string}`]: TOptional<TString>
[key: `orderby.${string}`]: TOptional<TString>
}>({
search: Type.Optional(Type.String()),
fields: Type.Optional(Type.String()),
page: Type.Optional(Type.Number({ minimum: 1 })),
pageSize: Type.Optional(Type.Number({ minimum: 1 })),
}, {
additionalProperties: false,
patternProperties: {
'^where.(.*)': Type.String(),
'^orderby.(.*)': Type.String(),
}
})
@climba03003 Heya
For now, I just replace the generic with what I expect.
Hmm, It might be possible to produce a utility type here. Below is a ObjectExtended
utility type that will let you compose Object with Record types. This should remove the need for the explicit annotation and let you compose other object types.
import { Type, Static, TSchema, TUnsafe, TObject, TRecord, TOptional, TNumber, TString, Evaluate } from '@sinclair/typebox'
//-------------------------------------------------------------------
// ObjectExtended
//-------------------------------------------------------------------
export type ObjectExtended<T extends TSchema[], Acc extends unknown = unknown> = (
T extends [infer L extends TSchema, ...infer R extends TSchema[]]
? ObjectExtended<R, Acc & Static<L>>
: TUnsafe<Evaluate<Acc>>
)
function ObjectExtended<T extends TObject, R extends TRecord[]>(object: T, records: [...R]): ObjectExtended<[T, ...R]> {
const patternProperties = records.reduce((acc, c) => ({...acc, ...c.patternProperties }), {})
return { ...object, patternProperties } as never
}
//-------------------------------------------------------------------
// Usage
//-------------------------------------------------------------------
export type CommonQueryString = Static<typeof CommonQueryString>
const CommonQueryString = ObjectExtended(Type.Object({
search: Type.Optional(Type.String()),
fields: Type.Optional(Type.String()),
page: Type.Optional(Type.Number({ minimum: 1 })),
pageSize: Type.Optional(Type.Number({ minimum: 1 })),
}, { additionalProperties: false }), [
Type.Record(Type.TemplateLiteral('where.${string}'), Type.String()),
Type.Record(Type.TemplateLiteral('orderby.${string}'), Type.TemplateLiteral('${ascending|descending}')),
Type.Record(Type.TemplateLiteral('groupby.${string}'), Type.String()),
])
It's a bit specific to extending Objects with Records (so couldn't include as part of TypeBox in it's current form), but should serve as a reasonable workaround if you need something a bit more flexible / generic.
Might close up this issue for now as there isn't much I can action in TypeBox today. But give the above a try and let me know how you go. Ill need to give compositions of this sort a review in future, but probably inline with Type.Evaluate (when/if lands) :)
All the best! S
I knows that I can use
Type.Intersect
for the same effect. But usingType.Composite
to generate singleobject
schema provides better performance infastify
.For example the below querystring, the
types
is incomplete and missing thepatternProperties
in result.Alternatives
It would be easier to directly support
patternProperties
throughType.Object
in some way. So, it can eliminate the usages ofType.Composite
.