Replies: 3 comments
-
Beta Was this translation helpful? Give feedback.
-
@incutonez Hi, TypeBox can be made to work in a OOP way, however it is recommended to avoid combining TSchema instances with Class instances via Object.assign (as this will likely cause all sorts of problems with TS inference, as well as pollute the TSchema schematic (which may break validators)). The current best approach is wrap the TSchema instance as a private field of a class, and access schema properties by unwrapping the schema in some way. const T = new WrappingClass(Type.String())
const type = T.Unwrap().type // 'string' - unwrap to access the schema properties The following is an implementation of this technique that layers the TB type system with a class based FluentType. This type implements combinators and functions similar to the Zod library, but can be extended to support additional functionality, including framework integration by passing framework objects via constructor. The following is a draft implementation (just copy and paste this somewhere, example usage included) import { Value } from '@sinclair/typebox/value'
import * as TB from '@sinclair/typebox'
// ---------------------------------------------------------------------------------------
// FluentType
// ---------------------------------------------------------------------------------------
export type FluentProperties = Record<PropertyKey, FluentType>
export type FluentRest = FluentType[]
export type FluentPropertiesToProperties<T extends FluentProperties> = TB.Evaluate<{
[K in keyof T]: ReturnType<T[K]['Schema']>
}>
export type FluentRestToRest<T extends FluentType[], Acc extends TB.TSchema[] = []> =
T extends [infer L extends FluentType, ...infer R extends FluentType[]]
? FluentRestToRest<R, [...Acc, ReturnType<L['Schema']>]>
: Acc
export class FluentType<T extends TB.TSchema = TB.TSchema> {
readonly #schema: T
constructor(schema: T) {
this.#schema = schema
}
// ------------------------------------------------------------------
// Methods
// ------------------------------------------------------------------
public Schema(): T { // Unwrap
return this.#schema
}
public Create(): TB.Static<T> {
return Value.Create(this.#schema)
}
public Check(value: unknown): value is TB.Static<T> {
return Value.Check(this.#schema, value)
}
public Parse(value: unknown): TB.Static<T> {
return Value.Decode(this.#schema, value)
}
// ... todo: add more methods
// ------------------------------------------------------------------
// Combinators
// ------------------------------------------------------------------
public And<FT extends FluentType, S extends TB.TSchema = ReturnType<FT['Schema']>>(FT: FT): FluentType<TB.TIntersect<[T, S]>> {
return new FluentType(TB.Intersect([this.#schema, FT.Schema() as S]))
}
public Or<FT extends FluentType, S extends TB.TSchema = ReturnType<FT['Schema']>>(FT: FT): FluentType<TB.TUnion<[T, S]>> {
return new FluentType(TB.Union([this.#schema, FT.Schema() as S]))
}
public Index<K extends PropertyKey[], S extends TB.TSchema = TB.TIndex<T, K>>(K: [...K]): FluentType<S> {
const S = TB.Index(this.#schema, K) as never as S
return new FluentType(S) as FluentType<S>
}
// ... todo: add more combinators
}
// --------------------------------------------------------------------
// FluentType Builder
// --------------------------------------------------------------------
export namespace Type {
export function Union<T extends FluentType[], R extends TB.TSchema[] = FluentRestToRest<T>>(types: [...T]): FluentType<TB.TUnion<R>> {
const R = types.map(type => type.Schema()) as R
return new FluentType(TB.Union(R)) as never
}
export function Intersect<T extends FluentType[], R extends TB.TSchema[] = FluentRestToRest<T>>(types: [...T]): FluentType<TB.TIntersect<R>> {
const R = types.map(type => type.Schema()) as R
return new FluentType(TB.Intersect(R)) as never
}
export function Tuple<T extends FluentType[], R extends TB.TSchema[] = FluentRestToRest<T>>(types: [...T]): FluentType<TB.TTuple<R>> {
const R = types.map(type => type.Schema()) as R
return new FluentType(TB.Tuple(R)) as never
}
export function Object<T extends FluentProperties, P extends TB.TProperties = FluentPropertiesToProperties<T>>(properties: T): FluentType<TB.TObject<P>> {
const P = globalThis.Object.getOwnPropertyNames(properties).reduce((acc, key) => ({ ...acc, [key]: properties[key].Schema() }), {} as P)
return new FluentType(TB.Object(P))
}
export function Array<T extends FluentType, S extends TB.TSchema = ReturnType<T['Schema']>>(type: T): FluentType<TB.TArray<S>> {
const S = type.Schema() as S
return new FluentType(TB.Array(S))
}
export function String() {
return new FluentType(TB.String())
}
export function Number() {
return new FluentType(TB.Number())
}
export function Boolean() {
return new FluentType(TB.Boolean())
}
// todo: add more types
}
// --------------------------------------------------------------------
// Class: Example
// --------------------------------------------------------------------
const T = new FluentType(TB.Object({
x: TB.Number(),
y: TB.Number(),
z: TB.Number(),
}))
const value = T.Parse({ x: 1, y: 2, z: 3 })
// --------------------------------------------------------------------
// Builder: Example
// --------------------------------------------------------------------
const A = Type.Object({
x: Type.Number(),
y: Type.Number(),
z: Type.Number(),
})
const B = A.And(Type.Object({
a: Type.String(),
b: Type.String(),
c: Type.String(),
}))
console.log(B.Schema()) This approach is somewhat different to the Object.assign() approach (and certainly more involved). But is the general recommendation for approaching OOP type compositing with TypeBox. There should be enough here to experiment and shape the implementation for various use cases though. Hope this helps |
Beta Was this translation helpful? Give feedback.
-
Hello, first, let me say thank you very much for taking the time to answer with such detail. I've seen your responses in every thread I've read, and I can't say I've ever seen a maintainer so active, so I appreciate what you do! Second, that's what I was thinking. However, I'm not sure how I would be able to get this working in Vue's reactivity system... it seems like the Static response messes it up. It sounds like I'm never supposed to be using the response from the create method, but that seems counterintuitive, so maybe I'm just doing something very wrong. I'll have to continue looking into it. Thank you for the example, it was very helpful! |
Beta Was this translation helpful? Give feedback.
-
Howdy! I think this may've been asked before, but in different ways that aren't necessarily the same as my use case, unless I'm really bad at querying. I've also read through the docs, and I might be missing something.
What I'm trying to do is use classes with TypeBox, and if I can't do that, somehow add base functions to all my TypeBox objects. I'm willing to let OOP go and use some functional programming hybrid, but I can't seem to solve it in either scenario. Let's dive in to some code (apologies if this is a bit over the place and pseudocode-y).
Let's say I have the following base class that I'd like to apply to all my model classes.
And let's say I have a Person model that would extend this.
I get why the 2nd console.log doesn't work because Object.assign doesn't auto-magically add the properties to the class's definition. I tried hacking around this some more, maybe adding something like:
This produces a TS Error about firstName and lastName not being implemented in Person... right, because they're required fields. Okay, I could make them optional, but that's not the right way to go. I'd rather not have to redefine these properties because I don't like maintaining 2 types for the same property. I could make a static create in the Person class instead...
...but then we get the opposite issue. I then tried to tinker with adding custom methods in Type.Object, but all seemed futile... I think I could create Type.Functions with default values, but that felt really wrong, and I don't even think it'd work because I couldn't reference the instance's data. It also wouldn't help if I was trying to actually access class properties in some computed methods in the Person class, because they still wouldn't be seen on the class, due to me not properly implementing them.
Is there a way to solve this issue?
Beta Was this translation helpful? Give feedback.
All reactions