Can schemas be extended? #602
-
I'm just starting to use TypeBox in a monorepo, and liking it so far. One thing I'd like to do is "bundle" compiled schemas along with helper methods and properties, so that I can import a single object that contains everything I need to work with a schema. I was thinking of something like this: // bind the methods of the compiled schema so we
// don't have to import the compiled schema separately
// Without this, we'd have to do something like:
// import { Value } from "@sinclair/typebox/value";
// import { MySchema, MySchemaTypeChecker, MySchemaExtensions } from './my-schema.js'
export type ExtendedSchema<T extends TSchema, E extends {}> = {
check: (value: unknown) => value is Static<T>;
errors: (value: unknown) => string[];
cast: (value: unknown) => Static<T>;
} & T & E;
export function createExtendedSchema<T extends TSchema, E extends {}>(schema: T, extensions:E): ExtendedSchema<T, E> {
const compiled = TypeCompiler.Compile(schema);
const check = compiled.Check;
const errors = (value: unknown) =>
[...compiled.Errors(value)].map(serializeError);
const cast = (value: unknown) => commpiled.Cast(schema, value);
return { schema, check, error, allErrors, cast, ...extensions };
} That approach seems to fail due to this error: So I'm assuming that "extending" a schema with arbitrary values is a no-no. Is there a better way to do this? Is there some other more idiomatic way to handle this that I'm missing because I'm a TypeBox newbie? P.S. - I know that I could bundle everything into one container object like: type SchemaBundle<T, E> = {
schema: T,
typeChecker: TypeCheck<T>
cast: (value: unknown) => Static<T>;
} & E I've tried that but while it cleans up the imports it makes using the bundles more cumbersome. Was curious if there was a better solution I was missing. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
@Ghirigoro Hi, Extra MetadataYou're free to add additional metadata (including functions) to a schema by passing them on schema options. const T = Type.String({
foo: () => 'hello world'
}) TypeBox doesn't limit additional metadata passed via options on a schema, but adding additional properties or functions may render that schema at odds with the specification (so is generally advised against). However it can be useful in documentation generators, form design time metadata, etc, but it's up to the developer to trade off specification alignment for more pragmatic constructs embedded in the schema (for example built in check functions) Check, Cast, EtcGenerally, it's not recommended to include check, cast, etc on the schematics, as all the information required to validate is encoded inside the schematic itself. Downstream consumers of the schematic may want to use Ajv or other standard compliant validator, so embedding check functions into the schema partially forces the consumer to use those functions, more often than not, it's better to just export the schematic / type and let the consumer choose the technology to validate it. This said, you can achieve this in the following way. import { Value } from '@sinclair/typebox/value'
import { Type, Static, StaticDecode, StaticEncode, TSchema } from '@sinclair/typebox'
import { TypeCompiler } from '@sinclair/typebox/compiler'
export type TExtendedSchema<T extends TSchema> = T & {
check(value: unknown): value is Static<T>
decode(value: unknown): StaticDecode<T>
encode(value: unknown): StaticEncode<T>
}
export function Extend<T extends TSchema>(schema: T): TExtendedSchema<T> {
const check = (value: unknown) => Value.Check(schema, value)
const decode = (value: unknown) => Value.Encode(schema, value)
const encode = (value: unknown) => Value.Decode(schema, value)
return { ...schema, check, decode, encode } as unknown as TExtendedSchema<T>
}
const T = Extend(Type.String()) // TExtendedSchema<TString>
type T = Static<typeof T> // string
const C = TypeCompiler.Code(T) // compiles ok
console.log(T, C) There are actually several ways to extend TB schematics for a variety of designs and use cases, but in my experience, it's generally better and cleaner to just keep the schematics as they are with minimal modification. Libraries like Zod, Yup, etc do couple type representation and validation as one in the same thing, but TypeBox has a very strong distinction between type representation (as Json Schema) and the code that validates those schemas (which could be any Json Schema compliant validator), and it's usually better to keep that distinction in place (as it's a fairly central principle to the TB library) Hope this helps! |
Beta Was this translation helpful? Give feedback.
@Ghirigoro Hi,
Extra Metadata
You're free to add additional metadata (including functions) to a schema by passing them on schema options.
TypeBox doesn't limit additional metadata passed via options on a schema, but adding additional properties or functions may render that schema at odds with the specification (so is generally advised against). However it can be useful in documentation generators, form design time metadata, etc, but it's up to the developer to trade off specification alignment for more pragmatic constructs embedded in the schema (for example built in check functions)
Check, Cast, Etc
Generally, it's not recommended to …