Building generic types #1204
Replies: 1 comment 2 replies
-
@daedalus28 Hi,
TypeBox generics programming has been evolving alongside the advancements in the TypeScript type system and the emerging techniques and idioms being established within the type-level programming community. Given the complexity of this topic and its evolving nature, it has been challenging to document. My preference so far has just been to let users experiment and to answer questions around it. This said, I would be happy to start an open dialog around generics programming in TypeBox. See below for reference implementations of SetOptional and Merge types import { Type, TObject, TProperties, SchemaOptions } from '@sinclair/typebox'
import { Syntax } from '@sinclair/typebox/syntax'
// ------------------------------------------------------------------------
// SetOptional
//
// When using a tuple as a parameter to a mapping, ensure the tuple is spread
// via [...T]. This causes TS to expand the tuple into a fixed sized array.
// This is important as all TypeBox types require fixed sized tuples to perform
// tuple remapping.
//
// ------------------------------------------------------------------------
const SetOptional = <Type extends TObject, Key extends PropertyKey[]>(type: Type, fields: [...Key], options?: SchemaOptions) =>
// tip: use Composite() instead of Intersect() to flatten the intersection. In future, Evaluate(Intersect([...T])) will
// become available to offer a more generalized version of Composite()
Type.Intersect([ Type.Omit(type, fields), Type.Partial(Type.Pick(type, fields))], options)
const A = SetOptional(Syntax(`{
x: number,
y: number,
z: number
}`), ['x', 'y'])
// ------------------------------------------------------------------------
// Merge
//
// Some types can't be naturally derived through JS logic. For a Merge
// operation, there is no equivalent in TS as it is based on sets and JS uses
// property key override in Right. In cases like this you need to replicate
// the JS logic at a type level. The TMerge type achieves this. Note the
// symmetry between the TS and JS implementations.
//
// ------------------------------------------------------------------------
type TMerge<Left extends TObject, Right extends TObject,
Properties extends TProperties = Omit<Left['properties'], keyof Right['properties']> & Right['properties'],
Evaluated extends TProperties = {[Key in keyof Properties]: Properties[Key]} & {}, // evalulated: prettify trick
Result extends TObject = TObject<Evaluated>
> = Result
const Merge = <Left extends TObject, Right extends TObject>(left: Left, right: Right, options?: SchemaOptions): TMerge<Left, Right> => {
const properties = { ...left.properties, ...right.properties }
const result = Type.Object(properties, options)
return result as never // computed type can't be inferred from JS logic. Use 'as never' and trust TMerge<...> return type
}
// override 'x', append 'w'
const B = Merge(Syntax(`{
x: number,
y: number,
z: number
}`), Syntax(`{
x: string,
w: boolean
}`)) If you have any specific questions on generic programming with TypeBox (i.e. tuple mapping, computed types, recursive inference techniques, etc), I'm happy to field these questions on this thread. Cheers! |
Beta Was this translation helpful? Give feedback.
-
I'm not clear how to build generic types properly. I don't see an ecosystem of utility types nor documentation beyond a very basic note, so I figured I'd open a discussion. I have two use cases, both inspired by utility types from type-fest. My goal is to understand how to create these types of utilities in addition to figuring out the two use cases at hand. These could definitely end up as PRs to typebox or as a new repo of typebox utilities.
The first is
SetOptional
, which is likeType.Partial
but applied to a list of fields. The pattern I'm generalizing is omitting a list of fields from a type and then intersecting with a partial pick of those fields:However, typing isn't preserved when I use this as typescript thinks this makes everything optional. It works perfectly inline when not wrapped in a function, so I suspect my typing for
fields
must be wrong somehow and causing type info to be lost.My second use case is
Merge
, which is similar but overrides some properties with new ones. This is what I have so far:The typing here just causes everything to get lost. Once again, inlining this makes everything work so I'm sure I just have the typings wrong.
Documenting a few of these examples would make these types of utilities more accessible.
Beta Was this translation helpful? Give feedback.
All reactions