Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How should TypeBox types be used with TS type predicates? #915

Closed
nivekh opened this issue Jun 24, 2024 · 2 comments
Closed

How should TypeBox types be used with TS type predicates? #915

nivekh opened this issue Jun 24, 2024 · 2 comments

Comments

@nivekh
Copy link

nivekh commented Jun 24, 2024

If I have a TS type predicate function which is meant to generically perform a TypeBox typecheck, e.g... (minimal example)

function handleGet(requestBody: unknown){
    const GetSchema = Type.Object(
       { id: Type.Integer() }, 
       { additionalProperties: false, title: 'GetSchema' }
    );
    type GetSchema = Static<typeof GetSchema>
    
    if(ValidateBody(GetSchema, requestBody)){
       // perform get action
    } else {
       // log validation error
    }
}

function ValidateBody(schema: TSchema, body: unknown): body is Static<typeof schema>{
    Value.Default(schema, body);
    const typechecker = TypeCompiler.Compile(schema);
    if (typechecker.Check(body)){
        return true;
    }
    return false;
}

...the final type that ends up getting 'confirmed' by the predicate is unknown.

image

Is there a better way to write this sort of "generic type predicate" function in tandem with TypeBox?

@sinclairzx81
Copy link
Owner

@nivekh Hi,

You can achieve this by using generic arguments on ValidateBody. I've added a couple of extra value function calls if you're interested. Additionally, I've updated Compile to Check (as this will be more performant than compiling each time)

import { Value } from '@sinclair/typebox/value'
import { Type, Static, TSchema } from '@sinclair/typebox'

function ValidateBody<T extends TSchema>(schema: T, body: unknown): body is Static<T> {
  // note: you can optionally process the value with additional value
  // operations. Default, Convert and Clean are very common.
  const defaulted = Value.Default(schema, body)
  const converted = Value.Convert(schema, defaulted)
  const cleaned = Value.Clean(schema, converted)

  // note: you should use Value.Check instead of compiling the schema
  // for each check. This will be more performant as compilation is
  // very expensive.
  return Value.Check(schema, cleaned)
}


const R = ValidateBody(Type.String(), null) // body is string

If you want to keep Compile, it's recommended to use a closure to hold onto the compiled schematics. The following updates the above to use the TypeCompiler + Function closure.

import { Value } from '@sinclair/typebox/value'
import { TypeCompiler } from '@sinclair/typebox/compiler'
import { Type, Static, TSchema } from '@sinclair/typebox'

function CompileSchema<T extends TSchema>(schema: T) {
  const check = TypeCompiler.Compile(schema)
  return (body: unknown): body is Static<T> => {
    const defaulted = Value.Default(schema, body)
    const converted = Value.Convert(schema, defaulted)
    const cleaned = Value.Clean(schema, converted)
    return check.Check(cleaned)
  }
}

const ValidateBody = CompileSchema(Type.String())

const R = ValidateBody(null) // (body: unknown) => body is string

Hope this helps
S

@nivekh
Copy link
Author

nivekh commented Jun 24, 2024

I can't thank you enough for the detailed reply, that helps a ton! I came close to the generic setup you've demonstrated when I was experimenting, but I'm still learning new things about TypeScript, so I was kind of approaching it backwards, I think.

I actually had a caching setup for the compiled validators which I didn't include for clarity's sake, but I'll have to play around with Value.Check instead and see if it doesn't beat the comp-and-cache approach (as I imagine it might?)

Thanks again, you've been a huge help!

@nivekh nivekh closed this as completed Jun 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants