Create schemas from type predicates
Hey! Awesome work with this project and it's been massively helpful :)
Could it be possible to define a type schema from a TypeScript type predicate?
Something like this is what I have in mind, I'm curious if there's a way to achieve this now or what you think about adding it to TypeBox (I'd be happy to create a PR).
// type predicate (https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards)
export function isMyType(value: unknown): value is MyType {
return typeof value === "number" && Number.isInteger(value) && value >= 0
// ...or some other custom type-checking logic
}
// tagged intersection (https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#type-aliases)
export type MyType = number & { __myType: never }
// defining a schema based off the predicate (the type is inferred to be TPredicate<MyType>)
const MyTypeSchema = Type.Predicate(isMyType)
And potential signatures:
export interface TPredicate<T> extends TSchema {
[Kind]: 'Predicate'
static: T
// maybe some others...?
}
// Type.Predicate(...) schema function
export function Predicate<T>(
predicate: (value: unknown) => value is T,
options: SchemaOptions = {},
): TPredicate<T> {
// ...
}
@BenWoodworth Hi! Glad you're finding the library useful :) And thanks for the suggestion!
I do actually think TypeBox needs something like this for user defined schemas that do not fall under the standard subset, however the caveat would be the predicate schema would need to be serializable. Unfortunately, encoding runtime logic inside Type.Predicate() would result in that logic being lost on serialization (even though encoding runtime logic inside the schema would be super convenient from a compiler and value check standpoint)
Currently, the design for custom schemas is to use Type.Unsafe() with Value.Check() and TypeCheck.Check() acting as type predicates. As of 0.24.x, the following works from a inference type perspective, just not from a functional runtime perspective (as custom schemas are yet to be implemented)
import { Value } from '@sinclair/typebox/value'
import { Type } from '@sinclair/typebox'
const T = Type.Unsafe<{ // inference type
a: string,
b: string,
c: string
}>({ // schema data
type: 'custom'
})
const value: unknown = null // some value to assert
if(Value.Check(T, value)) { // checks are type predicates
value.a.toLocaleLowerCase()
value.b.toLocaleLowerCase()
value.c.toLocaleLowerCase()
}
So, in the above, it's possible to encode arbitrary and serializable information for the schema, but the check will fail because the custom schema is unknown and outside the standard subset. The plan for adding the actual logic to test the value may look something like the following. (which may somewhat mirror the design of Formats)
import { Value } from '@sinclair/typebox/value'
import { Type } from '@sinclair/typebox'
import { Predicate } from '@sinclair/typebox/predicate'
// -------------------------------------------------------
// Globally Register Schema
// -------------------------------------------------------
Predicate.Set((schema, value) => {
return (
typeof schema === 'object' && schema !== null &&
typeof schema.type === 'string' && schema.type === 'custom' &&
typeof value === 'object' && value !== null &&
typeof value.a === 'string'
typeof value.b === 'string'
typeof value.c === 'string'
)
})
const T = Type.Unsafe<{ // inference type
a: string,
b: string,
c: string
}>({ // schema data
type: 'custom'
})
const value: unknown = null // some value to assert
if(Value.Check(T, value)) { // checks are type predicates
value.a.toLocaleLowerCase()
value.b.toLocaleLowerCase()
value.c.toLocaleLowerCase()
}
Would something like the above provide similar functionality to the envisioned Type.Predicate() ?
Cheers
S
This is exactly how I imagined supporting runtime validation for custom/unsafe types would work. In some ways, this custom predicate approach is just a more generalized version of the "Formats" API so it makes complete sense to mimic its API or even merge the concepts somehow.
I know there is desire to support Cast/Create as well for unsafe types (although I think it would be reasonable to not support those features). Really though, adding support doesn't make things much more complicated for the library, it just makes the API a bit more cumbersome for the consumer since additional functions would need to be provided to perform/check the cast and produce new default values.
Perhaps a middle ground would be to allow the registration of these unsafe types with Cast/Create/Check independently, so that the consumer gets to decide where they need support. I imagine Check support would be significantly more important than Create for most use cases, and you've already done such a good job of grouping these different features under separate exports.
@BenWoodworth @jayalfredprufrock Have just pushed the first revision of custom types in TypeBox. Documentation for using these types can be located here. Updates are on 0.25.9.
Example
The following example shows creating a custom type to validate JavaScript bigint values. Note that to create a custom type, the type needs to include the [Kind] property. The Custom.Set('<kind>', value => { ... }) accepts the Kind value on the first argument. The setup is very similar to the existing Format module.
import { Type, Kind } from '@sinclair/typebox'
import { Custom } from '@sinclair/typebox/custom'
import { TypeCompiler } from '@sinclair/typebox/compiler'
// Register Type with the Custom namespace
Custom.Set('BigInt', value => typeof value === 'bigint')
// Create a Type schema using the [Kind] property
const T = Type.Unsafe<bigint>({ [Kind]: 'BigInt' })
// Compile it
const C = TypeCompiler.Compile(T)
// Check it
console.log(C.Check(100n)) // true
This functionality has been implemented for the TypeCompiler and Value API's. Error reporting is current limited to only report there was a Custom error (although the report includes the schema so custom error mapping is possible)
Will close off this issue for now Cheers S