Closed gotjoshua closed 1 year ago
I discovered that there are some relevant examples here: https://github.com/sinclairzx81/typebox/blob/master/example/collections
still curious for your thoughts about including the pattern in the lib in a more core way than examples...
@gotjoshua Hi!, Nice work on the POC! it's great to see implementations like this that experiment with high level usage. Good work :)
still curious for your thoughts about including the pattern in the lib in a more core way than examples...
Yeah it's quite unlikely TypeBox will add additional functionality above and beyond what it's currently offering. TypeBox is generally more aligned at being a relatively low level type system and providing just enough "opt-in" functionality to make higher level abstractions possible (but stops short of actually implementing any of them, noting the collections
example is just a reference for what higher level abstractions could look like).
The reasoning for not including more concrete abstractions, is there are many different (and often contentious) ways to express higher level functionality (but usually only a few ways to express the operations underneath). For example, functional libraries such as lodash
, ramba
, etc all express .map()
, but there's only one way to actually implement it. In much the same way, there is only one way to express a string
schema, and only one way to validate it. TypeBox tries to extend this principle all the way down to type composition itself.
const T = { type: 'string', exlusiveMinimum: 10 } // the rawest implementation. (abstraction 0)
const T = Type.String({ exclusiveMaximum: 10 }) // string + optional constraints (abstraction 1)
const T = z.string().lt(10) // fluent pattern (abstraction 2)
Where raw type expressions can more easily map to fluent (or other) patterns, but the inverse is not always necessarily true (especially for more sophisticated types). For this reason, TypeBox opts for the lowest possible expression that still provides hooks to reconcile the type with the TS type system.
In projects where I'm using TypeBox, the following pattern is generally how I go about approximating something like ObjectModel (which you've discovered with your POC). The following is some generic Model<T>
with type safe set
.
import { TypeCompiler } from '@sinclair/typebox/compiler'
import { TSchema, Static } from '@sinclair/typebox'
export class Model<T extends TSchema> {
#check: TypeCheck<T>
constructor(private readonly schema: T) {
this.#check = TypeCompiler.Compile(schema) // compilation tied to object creation
}
public set(value: Static<T>): void {
if(!#check.Check(value)) throw Error('invalid')
}
public get(): Static<T> {}
}
Of course, the above can be extended to any class operating on values of generic type T
where a TSchema
is passed as an argument on the classes constructor (which is enough to hide much of the Static<T>
inference for the users of the class)
const queue = new Queue(Type.String())
const [sender, receiver] = new Channel(Type.String())
const publisher = new AwsSqsPublisher(Type.String())
const table = new Table(Type.Object({
id: Type.Integer(),
name: Type.String()
}))
const commandLineArgs = Cli.Parse(Type.Object({
strict: Type.Boolean(),
target: Type.Union([
Type.Literal('ESNext'),
Type.Literal('ES2022')
])
})
Given the number of ways TypeBox can be integrated into applications and infrastructure, keeping things low level generally provides the most options to implement checks in ways aligned to the surrounding infrastructure.
Hope this helps! S
Hope this helps!
indeed! many thanks!
i wonder if a sidecar library with Typebox as a peer dep, would make sense.
I'd need to be sure the patterns are sound and well documented, but it could prove useful.
constructor(private readonly schema: T)
could you possibly give a quick typescript lesson about this private readonly
constructor arg?
edit: found this ref
@gotjoshua Hi
could you possibly give a quick typescript lesson about this private readonly constructor arg?
Ah, this is just a convenience syntax for class field assignment in TypeScript. Not sure of the specific name of the syntax, I've generally just called them primary constructors in recent years (borrowing on C#'s definition for them)
class Foo {
private readonly value: string
constructor(value: string) {
this.value = value
}
}
can be written shorthand where the this.value = value
is implicit
class Foo {
constructor(private readonly value: string) {}
}
i wonder if a sidecar library with Typebox as a peer dep, would make sense.
Yeah, maybe. I think there's potential to explore implementing type safe abstractions across quite a lot of JavaScript (not only Map, Set, Array and Atom), but for things like functions also. Here's a rough prototype.
import { Typed } from '<package-name-here>'
// collections + atom (renamed to value)
const V = new Typed.Value(Type.Number()) // number (i.e. proxied Atom)
const M = new Typed.Map(Type.String(), Type.Number()) // Map<string, number>
const S = new Typed.Set(Type.Number()) // Set<number>
const A = new Typed.Array(Type.Number()) // Array<number>
// function: signature
const F = Type.Function([ // (a: number, b: number) => number
Type.Number(),
Type.Number()
], Type.Number())
// function: implementations
const add = Typed.Function(F, (a, b) => a + b)
const sub = Typed.Function(F, (a, b) => a - b)
const mul = Typed.Function(F, (a, b) => a * b)
const div = Typed.Function(F, (a, b) => a / b)
const mod = Typed.Function(F, (a, b) => a % b)
// i.e: add(1, 2) === 3
// constructor: signature
const C = Type.Constructor(() => Type.Object({ // new () => {
add: F // add: (a: number, b: number) => number
sub: F // sub: (a: number, b: number) => number
mul: F // mul: (a: number, b: number) => number
div: F // div: (a: number, b: number) => number
mod: F // mod: (a: number, b: number) => number
}) // }
// constructor: implementation
const math = new Typed.Constructor(C, { add, sub, mul, div, mod })
// i.e: math.add(1, 2)
There might be more abstractions possible, these are the ones that immediately spring to mind.
Hey, might close off this issue for now, but happy to discuss higher level abstractions in more depth (either here or on GH discussions). I think a sidecar library would be quite interesting to explore and may prove quite useful for some application types (particularly for NPM libraries that expose interfaces to callers; but can't easily assert the caller is calling that interface correctly). Using types + assertions (as above) would offer both validation and reflectable interface documentation. Something like that could be quite cool and is worth thinking about :)
All the best! S
I am a long term user and fan of Object Model and recently asked the author if he wanted to add Object Model to @samchon 's comparisons
His reply was a very insightful metaphor
This triggered a poc exploration of using typebox to create proxys that check on every assignment https://gitlab.com/onezoomin/ztax/typebox-trpc-plasmo/-/commit/012c7dfef0902b37920c164953941e4c27521062#note_1300058374
Previously I had already created a pattern for creating a class that does checking and casting or throwing in the constructor: https://gitlab.com/onezoomin/ztax/typebox-trpc-plasmo/-/blob/trunk/model/utils.ts#L28-51
So, I open this issue as a show and tell, and with the question to @sinclairzx81 : Would you welcome a PR that implements these patterns as core typebox features? If not, are you willing to share some thoughts and philosophies why not?