Issue with generics #351
-
Hi, I am trying to implement a generic type that accepts a custom schema as an argument and builds a new schema that incorporates this custom schema. I have some issues that I mention in the code below. And another couple of questions:
import { Type, Static, TSchema } from '@sinclair/typebox';
import { Value } from '@sinclair/typebox/value';
export const GenericQuerySchema = Type.Object({});
export type GenericQuery = Static<typeof GenericQuerySchema>;
const BaseMessageSchema = Type.Object({
id: Type.Optional(Type.String()),
});
const createOptsSchema = <T extends TSchema>(querySchema: T) =>
Type.Composite([
BaseMessageSchema,
Type.Object({
querySchema: Type.Any(),
query: querySchema,
}),
]);
type OPTS<T extends TSchema> =
Static<ReturnType<typeof createOptsSchema<T>>>;
const test = <T extends TSchema>(options: OPTS<T>) => {
const optsSchema = createOptsSchema<T>(options.querySchema);
// ------------------------------------------^ Property 'querySchema' does not exist on type 'Evaluate<Evalu...
const errors = Value.Errors(optsSchema, options);
console.log('Errors: -->', errors.First());
options = Value.Cast(optsSchema, options);
console.log('Options: -->', options.id, options.query);
// ------------------------------------------^ Property 'query' does not exist on type 'Evaluate<Evalu...
};
const CustomQuerySchema = Type.Object({
hello: Type.String(),
});
test<typeof CustomQuerySchema>({
id: '123',
query: {
hello: 'qwerty',
},
querySchema: CustomQuerySchema,
});
|
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 2 replies
-
@kostysh Hi. Instantiation Expressions mixed with TypeBox types can be quite tricky to get right, but the following should implement something close to what you're trying to achieve. import { Type, Static, TSchema } from '@sinclair/typebox'
// ----------------------------------------------------------------------
// Generic Query Options
// ----------------------------------------------------------------------
export type Options<T extends TSchema> = Static<ReturnType<typeof Options<T>>>
export const Options = <T extends TSchema>(query: T) => Type.Composite([
Type.Object({ id: Type.String() }),
Type.Object({
querySchema: Type.Any(),
query
})
])
// ----------------------------------------------------------------------
// Methods
// ----------------------------------------------------------------------
function test<T extends Options<TSchema>>(options: T) {
const { id, query } = options // query is 'unknown' as TSchema is varying
}
function test_constrained<T extends Options<typeof T>>(options: T) {
const { id, query } = options // query is '{ a: number, b: number }'
}
// ----------------------------------------------------------------------
// Example
// ----------------------------------------------------------------------
const T = Type.Object({
a: Type.Number(),
b: Type.Number()
})
test<Options<typeof T>>({ // external dependent type
id: '123',
querySchema: {},
query: {
a: 1,
b: 1
}
})
test_constrained({
id: '123',
querySchema: {},
query: {
a: 1,
b: 1
}
}) Hope this helps! |
Beta Was this translation helpful? Give feedback.
-
@kostysh Hi
You're welcome, and thanks!
CastThe Cast function is similar to the Create function, but is different in that it accepts a "Partial" value and will populate missing values with defaults while retaining valid values. Cast was written specifically with data migrations in mind. Scenarios where you might use Cast is when you change a schematic and need to update existing values to match the new schema / type. const T = Type.Object({
x: Type.Number(),
y: Type.Number(),
z: Type.Number(),
})
const V = Value.Cast(T, { x: 42 }) // const V = { x: 42, y: 0, z: 0 } - retain { x: 42 } add { y: 0, z: 0 } ConvertConvert is a new function added on 0.26.0. It does value coercion to try and align a value to its target type. It's important to note that conversion can fail, and because of this, the Convert function will return import { Type } from '@sinclair/typebox'
import { Value } from '@sinclair/typebox/value'
const T = Type.Object({ x: Type.Number() }) // target type { x: number }
const C1 = Value.Convert(T, { x: '42' }) // const C1: unknown = { x: 42 } - x could be converted from '42' to 42
const C2 = Value.Convert(T, { x: 'hello' }) // const C2: unknown = { x: 'hello' } - x could not be converted
const R1 = Value.Check(T, C1) // const R1 = true - value conversion resulted in correct value
const R2 = Value.Check(T, C2) // const R2 = false - value conversion resulted in incorrect value
You can use Convert and Cast together in the following way const T = Type.Object({ x: Type.Number(), y: Type.Number() })
const convert = Value.Convert(T, { x: '42' }) // const convert = { x: 42 }
const casted = Value.Cast(T, convert) // const casted = { x: 42, y: 0 }
The simplest way to approach this is with a custom type. Below is a revised version of the import { TypeSystem } from '@sinclair/typebox/system'
import { Type, TSchema, Static, TypeGuard } from '@sinclair/typebox'
// ----------------------------------------------------------------------
// Generic Query Options + TSchema custom type
// ----------------------------------------------------------------------
const TSchema = TypeSystem.Type<TSchema>('TSchema', (_, value) => TypeGuard.TSchema(value))
export type Options<T extends TSchema> = Static<ReturnType<typeof Options<T>>>
export const Options = <T extends TSchema>(query: T) => Type.Composite([
Type.Object({ id: Type.String() }),
Type.Object({
querySchema: TSchema(),
query
})
])
// ----------------------------------------------------------------------
// Methods
// ----------------------------------------------------------------------
function test<T extends Options<TSchema>>(options: T) {
const { id, query, querySchema } = options
}
function test_constrained<T extends Options<typeof T>>(options: T) {
const { id, query, querySchema } = options
}
// ----------------------------------------------------------------------
// Example
// ----------------------------------------------------------------------
const T = Type.Object({
a: Type.Number(),
b: Type.Number()
})
test<Options<typeof T>>({
id: '123',
querySchema: Type.String(),
query: {
a: 1,
b: 1
}
})
test_constrained({
id: '123',
querySchema: Type.String(),
query: {
a: 1,
b: 1
}
}) There is also a way to express a runtime const Object = <T extends TSchema>(TSchema: T) => Type.Object({ type: Type.Literal('object'), required: Type.Optional(Type.Array(Type.String())), properties: Type.Record(Type.String(), TSchema) })
const Array = <T extends TSchema>(TSchema: T) => Type.Array(TSchema)
const String = Type.Object({ type: Type.Literal('string') })
const Number = Type.Object({ type: Type.Literal('number') })
const Boolean = Type.Object({ type: Type.Literal('boolean') })
const TSchema = Type.Recursive(TSchema => Type.Union([ // union all types of TSchema
Object(TSchema),
Array(TSchema),
String,
Number,
Boolean,
])) Hope this helps |
Beta Was this translation helpful? Give feedback.
-
@sinclairzx81 Tried to dive deeper into the case discussed above and stuck with the following issue. function test_constrained<T extends Options<typeof T>>(options: T) {
const { id, query, querySchema } = options
} this function test<T extends Options<TSchema>>(options: T) {
const { id, query, querySchema } = options;
// ...
return options;
} Let's look into the example. Here I have created a export type Message<T extends TSchema> = Static<ReturnType<typeof Message<T>>>
export const Message = <T extends TSchema>(query: T) => Type.Object({
id: Type.String(),
query
})
function test<T extends Options<TSchema>>(options: T) {
const { query, querySchema } = options
const message: Message<typeof querySchema> = {
id: '123',
query
};
return message;
}
const msgInternal = test<Options<typeof Query>>({
querySchema: Query,
query: {
a: 1,
b: 1
}
})
/**
// `msgInternal` type
type msgInternal = {
query: unknown;
id: string;
}
*/
type msgExternal = Message<typeof Query>;
/**
type msgExternal = {
query: {
a: number;
b: number;
};
id: string;
}
*/ |
Beta Was this translation helpful? Give feedback.
@kostysh Hi.
Instantiation Expressions mixed with TypeBox types can be quite tricky to get right, but the following should implement something close to what you're trying to achieve.
TypeScript Link Here