What is the rational behind making v0.34 Type.Ref not allow passing in a schema and inferring the type? #1071
-
The reason I've used Type.Ref is to shorten the generated json schemas. In some cases it removes thousands of lines of repeated code. Why add the breaking change to Type.Ref in v0.34? Why not allow backwards compatibility? Why not support type inference of reference types? Wrapping Type.Ref with Type.Unsafe to get type inference is at least a workaround but I'd really like to understand what made this change necessary in the first place. Thanks! |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 3 replies
-
@stuft2 Hi,
The change was made in support of the new Module feature which provides enhanced (and Safe) inference support for Ref. Unfortunately the previous Ref signature wasn't able to be repurposed in a way I felt comfortable maintaining moving forward (given conflicting designs). The updated signature on 0.34.0 is able to be made backwards compatible to 0.33.0, but the inverse wasn't possible. The call was made to drop the previous signature, but provide the fallback option via Unsafe.
You can replicate the old TRef using Unsafe (the following should work as before) import { Type, TSchema, Static, TUnsafe } from '@sinclair/typebox'
// Old Ref
function Ref<T extends TSchema>(schema: T): TUnsafe<Static<T>> {
return Type.Unsafe<Static<T>>(Type.Ref(schema.$id!))
}
// ...
const T = Type.String({ $id: 'T' })
const R = Ref(T) // { $ref: 'T' }
Type inference is supported for Ref, but only within the context of a Module where it's possible to prove to the type system that the target type is correct. const Module = Type.Module({
A: Type.Number(), // Auto assigned { $id: 'A' } from property name
R: Type.Ref('A'), // { $ref: 'A' }
})
// ...
type R = Static<typeof R> // type R = number
const R = Module.Import('R') Inference is also supported for mutually and self recursive types (something that wasn't possible before under the previous TRef design) const Module = Type.Module({
Node: Type.Object({
id: Type.String(),
nodes: Type.Array(Type.Ref('Node'))
})
})
// ...
type Node = Static<typeof Node> // type Node = {
// id: string;
// nodes: ...[];
// }
const Node = Module.Import('Node') FutureThe updates to Module and Ref are part of a larger body of work to establish better alignment with the TS type system, specifically to enable order independent type definitions to reference (to be) defined types. The updates to Ref enable this (as they just accept strings to remote targets). By consequence of this, Ref is able to be used as a parse target for the new Syntax Type feature (which also support out of order type referencing) import { Static } from '@sinclair/typebox'
import { Parse } from '@sinclair/typebox/syntax'
// B references A before A is defined
const Foo = Parse(`module Foo {
type B =
Pick<A, 'id'> &
Partial<Omit<A, 'id'>>
type A = {
id: string
nodes: A[]
}
}`)
const B = Foo.Import('B')
type B = Static<typeof B> // type B = {
// id: string;
// } & {
// nodes?: {
// id: string;
// nodes: ...[];
// }[] | undefined;
// } The Module, Ref (Computed) and Syntax features will be expanded upon throughout 0.34.x. I apologize if this change has caused any inconvenience. I had been hesitant to implement it for several months but ultimately decided to proceed because I believe the new Module features address more problems than the breaking change to Ref. I hope the update hasn't caused too many problems Happy to discuss more on this thread. |
Beta Was this translation helpful? Give feedback.
-
@sinclairzx81 Is your recommendation to strictly use the new Module feature going forward when needing to use Ref? If so, can you offer some guidance on how to go about doing this? I'm struggling with two problems. Schema reuseI have some schemas (e.g., enums) that are reused by other schemas that may not be related to each other. I can't use Ref unless I do one of the following:
None of these options feel quite right. Or maybe I'm missing something obvious? Auto-generated OpenAPI documentationI'm using TypeBox in a Fastify application, where I use plugins to generate OpenAPI documentation for my API routes. My understanding is that a Module is a schema with a |
Beta Was this translation helpful? Give feedback.
-
@liu-ronny Hi,
Ref is still intended to be used in isolation without Module (so you don't need to upgrade schematics to use Modules). However previous (and future) implementations using Ref will need to be mindful of a few subtle aspects with regards to the Ref API. const T = Type.Object({
x: Type.Number(),
y: Type.Number()
}, { $id: 'T' })
// ------------------------------------------
// Before 0.34.0
// ------------------------------------------
const R = Type.Ref(T) // const R = { $ref: 'T' }
type R = Static<typeof R> // type R = { x: number, y: number }
// ------------------------------------------
// After 0.34.0
// ------------------------------------------
const R = Type.Ref('T') // const R = { $ref: 'T' }
type R = Static<typeof R> // type R = unknown CHANGES: The 0.34.0 version of Ref requires you to pass the $ref parameter via Ajv & FastifyBy updating Ref to take the string parameter, this is all that's required to make things work from a runtime / validation standpoint (using Type InferenceTo retain inference, the recommended approach is to wrap the Ref in an Unsafe. The Unsafe type is used to override the default inference for any given TypeBox type. In the case of Ref, we need to use Unsafe to override the default inference of // ------------------------------------------
// Before 0.34.0
// ------------------------------------------
const R = Type.Ref(T) // const R = { $ref: 'T' }
type R = Static<typeof R> // type R = { x: number, y: number }
// ------------------------------------------
// After 0.34.0
// ------------------------------------------
const R = Type.Unsafe<Static<typeof T>>(Type.Ref('T')) // const R = { $ref: 'T' }
type R = Static<typeof R> // type R = { x: number, y: number } UnsafeRefAs with other TypeBox types, you can construct a generic utility type that can provide a factory for the wrapped Unsafe Ref. The following reimplements pre-0.34.0 Ref via the utility type UnsafeRef. import { Type, TSchema, TUnsafe, Static } from '@sinclair/typebox'
// Replicates pre-0.34.0 Ref
function UnsafeRef<Type extends TSchema>(type: Type): TUnsafe<Static<Type>> {
return Type.Unsafe<Static<Type>>(Type.Ref(type.$id!))
}
// ...
const T = Type.Object({
x: Type.Number(),
y: Type.Number(),
}, { $id: 'T' })
const R = UnsafeRef(T) // const R = { $ref: 'T' }
type R = Static<typeof R> // type R = { x: number, y: number } Why Ref changedThe main reason Ref changed was due to the following not being possible with the previous API const R = Type.Ref(T) // Error: Cannot use variable before initialization.
const T = Type.String({ $id: 'T' }) This is solved in 0.34.0 const R = Type.Unsafe<Static<typeof T>>(Type.Ref('T')) // this is fine.
const T = Type.String({ $id: 'T' }) ... so, the main reason for the change is to enable non-order dependent types to be created with reference types. The change here paves the way for upgrades to TypeBox's Recursive type implementation (due on the next release). Some of these upgrades are available via Module where deferred dereferencing via string parameter is facilitating both singular and mutual recursive schemas & inference. Unfortunately, the change did force Ref to infer as Hope this helps. There was a fair amount of consideration that went into the Ref type update, but outside the signature change (passing string instead of schemas), they should largely work as before. The need for the extra explicit mechanisms (Unsafe) may be ironed out in future revisions (for example, making UnsafeRef a built in type (possibly named something else)), but for now, you will need wrap the Ref in Unsafe manually. Happy to discuss more on this thread if you have any other questions |
Beta Was this translation helpful? Give feedback.
@stuft2 Hi,
The change was made in support of the new Module feature which provides enhanced (and Safe) inference support for Ref. Unfortunately the previous Ref signature wasn't able to be repurposed in a way I felt comfortable maintaining moving forward (given conflicting designs). The updated signature on 0.34.0 is able to be made backwards compatible to 0.33.0, but the inverse wasn't possible. The call was made to drop the previous signature, but provide the fallback option via Unsafe.
You can replicate the old TRef using Unsafe (the following should work as before)