-
Notifications
You must be signed in to change notification settings - Fork 142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Static resolution is not working as expected when using a generic function to generate a schema #813
Comments
@oshox Hi,
This mostly comes down to TypeScript not having adequate information to derive the correct types from the runtime implementation of I've had go reimplementing the type and runtime logic using similar techniques TypeBox uses internally. Generally the implementation of these kinds of mappings require you to define the source structures (Source Types) and the associated runtime / static logic that gradually remap the type at varying levels. Given the requirement to rename property names, and generate mappings of custom schematics, things can get a bit complex. The following implementation has been tested locally as should work ok. Scroll down for the TypeScript Inference Example Here import * as TB from '@sinclair/typebox'
// ------------------------------------------------------------------
// Source Types
// ------------------------------------------------------------------
type Descriptor = Readonly<{ type: 'number' | 'text', display: string }>
type Schema = Readonly<{ [key: string]: Descriptor }>
type Schemas = Readonly<{ [key: string]: Schema }>
// ------------------------------------------------------------------
// TFromDescriptor
// ------------------------------------------------------------------
type TFromDescriptor<T extends Schema, K extends PropertyKey> = K extends keyof T
? TB.Evaluate<Record<`${TB.Assert<K, string>}_filter`,
T[K]['type'] extends 'number' ? TB.TOptional<TB.TNumber> :
T[K]['type'] extends 'text' ? TB.TOptional<TB.TString> :
TB.TNever>>
: {}
function FromDescriptor<T extends Schema, K extends keyof T>(schema: T, key: K): TFromDescriptor<T, K> {
return {
[`${key as string}_filter`]:
schema[key]['type'] === 'number' ? TB.Optional(TB.Number(schema[key])) :
schema[key]['type'] === 'text' ? TB.Optional(TB.String(schema[key])) :
TB.Never()
} as never
}
// ------------------------------------------------------------------
// FromSchemaReduce
// ------------------------------------------------------------------
type TFromSchemaReduce<T extends Schema, K extends PropertyKey[], Acc extends Record<PropertyKey, TB.TSchema> = {}> = (
K extends [infer L extends PropertyKey, ...infer R extends PropertyKey[]]
? TFromSchemaReduce<T, R, Acc & TFromDescriptor<T, L>>
: TB.TObject<TB.Evaluate<Acc>>
)
function FromSchemaReduce<T extends Schema, K extends PropertyKey[]>(schema: T, keys: K): TFromSchemaReduce<T, K> {
const properties = keys.reduce((Acc, L) => {
return { ...Acc, ...FromDescriptor(schema, L as keyof T) }
}, {} as TB.TProperties)
return TB.Type.Object(properties, { additionalProperties: false }) as never
}
// ------------------------------------------------------------------
// FromSchema
// ------------------------------------------------------------------
type TFromSchema<T extends Schema, K extends PropertyKey[] = TB.UnionToTuple<keyof T>> = TFromSchemaReduce<T, K>
function FromSchema<T extends Schema>(schema: T): TFromSchema<T> {
return FromSchemaReduce(schema, Object.keys(schema)) as never
}
// ------------------------------------------------------------------
// TFromSchemas
// ------------------------------------------------------------------
type TFromSchemas<T extends Schemas> = TB.Evaluate<{
-readonly [K in keyof T]: TFromSchema<T[K]>
}>
function FromSchemas<T extends Schemas>(schemas: T): TFromSchemas<T> {
return Object.keys(schemas).reduce((Acc, K) => {
return { ...Acc, [K]: FromSchema(schemas[K]) }
}, {}) as never
}
// ------------------------------------------------------------------
// Usage
// ------------------------------------------------------------------
import { Static } from '@sinclair/typebox'
import { TypeCompiler } from '@sinclair/typebox/compiler'
const Mapped = FromSchemas({
foo: { text1: { type: "text", display: "Text 1" }, number1: { type: "number", display: "Number 1" } },
bar: { text2: { type: "text", display: "Text 2" }, number2: { type: "number", display: "Number 2" } }
} as const)
const fooSchema = Mapped["foo"]
type FooSchema = Static<typeof fooSchema>
const barSchema = Mapped["bar"]
type BarSchema = Static<typeof barSchema>
const fooValidator = TypeCompiler.Compile(fooSchema)
const barValidator = TypeCompiler.Compile(barSchema)
const fooObject = {
number1_filter: 123,
text1_filter: 'example'
}
const barObject = {
number2_filter: 13,
text2_filter: 'example2'
}
console.log('Valid object is valid:', fooValidator.Check(fooObject))
// console.log('Errors:', ...fooValidator.Errors(fooObject))
console.log('Invalid object is valid:', fooValidator.Check(barObject))
// console.log('Errors', ...fooValidator.Errors(barObject))
console.log('Valid object is valid:', barValidator.Check(barObject))
/// console.log('Errors:', ...barValidator.Errors(barObject))
console.log('Invalid object is valid:', barValidator.Check(fooObject))
// console.log('Errors', ...barValidator.Errors(fooObject)) Hope this helps |
@oshox Hiya, Might close off this issue as the above example should provide a good reference as to how to approach advanced type mapping in TypeBox. As noted, the inference isn't working here (as per original snippet) due to TypeScript being unable to derive the exact types from the function implementation. In cases like this you will need to help it along by writing explicit mappings (on the return type) that computes the anticipated output type. If you have any other questions, feel free to ping on this thread. |
Thank you for the response. You explanation solves my issue, and I will be able to implement those concepts in my other projects. |
In the following code, the 'Static' function is not correctly showing the output of my 'schema' function. Both validator functions work as expected, though:
(using 0.32.20)
The Static Types will show correctly after removing the type from schemaObject:
const schemasObject: Schemas = {
becomes
const schemasObjec = {
But then Typescript throws an error when I try to access a nested property:
switch (schema[keyName].type) {
Is this behavior expected and the result of me doing something wrong?
The text was updated successfully, but these errors were encountered: