sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.78k stars 152 forks source link

generics question: "T implements CustomType"? #340

Closed eropple closed 1 year ago

eropple commented 1 year ago

Hey there! Thanks for typebox, this library is awesome.

I've been digging through prior issues, and I feel like this has to be answered somewhere, but I'm at a loss for it. I'd appreciate some guidance with this problem. I've got a base type, defined in TypeBox, e.g.

export const Base = Type.Object({
  requiredField: Type.String(),
});
export type Base = Static<typeof Base>;

I then extend the type with Type.Intersect:

export const Derived = Type.Intersect([
  Base,  
  Type.Object({
    derivedField: Type.String(),
  })
]);
export type Derived = Static<typeof Derived>;

What I'm trying to do from here is genericize a function (in my case, JWT validation, but it extends to a lot of stuff) that accepts a schema that must implement Base, while returning Static<T extends Base> out of the function (that is, if passed Derived, it must return a Derived.

I assume this is a known pattern; is there an example of it somewhere? I'm just looking for a handhold from which to start, so if there's something useful to read, I'm very happy to. Thanks!

sinclairzx81 commented 1 year ago

@eropple Hi!

You can use functions to simulate generics in TypeBox. The common pattern is to pass a generic argument constrained to some type of TSchema (so <T extends TSchema> is very common). For example....

TypeScript Link Here

// -----------------------------------------------------------------
// TypeScript Generics
// -----------------------------------------------------------------
export type Vector<T> = { x: T, y: T, z: T }
export type BooleanVector = Vector<boolean>
export type NumberVector = Vector<number>

// -----------------------------------------------------------------
// TypeBox Generics
// -----------------------------------------------------------------
export const Vector = <T extends TSchema>(t: T) => Type.Object({ x: t, y: t, z: t })
export const BooleanVector = Vector(Type.Boolean())
export const NumberVector = Vector(Type.Boolean())

So from this, you can implement a Base and Derived in the following way. Note that for Intersect types, you will need to constrain to TObject.

TypeScript Link Here

import { Type, Static, TObject } from '@sinclair/typebox'

export const Base = <T extends TObject>(derived: T) => Type.Intersect([
  Type.Object({
    requiredField: Type.String(),
  }),
  derived
])

export type BaseDefault = Static<typeof BaseDefault> // optional: if you need to instance the base
export const BaseDefault = Base(Type.Object({}))     //           without any derived properties

export type Derived = Static<typeof Derived>
export const Derived = Base(Type.Object({
  derivedField: Type.String(),
}))

Hope this helps! S

eropple commented 1 year ago

Thanks for the quick reply! I think I've cracked it; my confusion was around the relationship between Base, typeof Base, and Static<typeof Base>. I was having trouble expressing how to go T extends (a TObject shaped like Base) and figured out that that's T extends typeof Base, which cracked it open for me.