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 to achieve better errros for discriminated unions #1017

Closed
AlbertMarashi opened this issue Oct 3, 2024 · 7 comments
Closed

How to achieve better errros for discriminated unions #1017

AlbertMarashi opened this issue Oct 3, 2024 · 7 comments

Comments

@AlbertMarashi
Copy link

AlbertMarashi commented Oct 3, 2024

I am aware there is an existing library for discriminated unions, but I am wondering if there is some kind of way to use the existing Value.Parse functionality such that it hints to the parser which item to use.

Zod had a proposed switch type which would effectively allow for the developer to determine switching logic for cases where there might be multiple or complicated discriminants.

Any plans to add something like this or be able to implement it using the existing systems?

eg:

Type.Switch(Type.Union([...]), value => {
  if (value.type === "Baz") return //do something
  return false // unknown union type
})
@AlbertMarashi
Copy link
Author

@AlbertMarashi
Copy link
Author

The kinda core issue here is that I get a very undescriptive error for union types:

Expected union value
Expected all values to match

@sinclairzx81
Copy link
Owner

@AlbertMarashi Hiya,

TypeBox doesn't support DiscriminatedUnion because they are not part of the Json Schema specification, however you can implement them manually. In the case of generating errors for them, you can override TypeBox's error generation function and intercept custom structures and create errors for them.

The following is an example of how you might approach this.

import { SetErrorFunction, DefaultErrorFunction } from '@sinclair/typebox/errors'
import { Type, KindGuard, ValueGuard, TSchema, TUnion } from '@sinclair/typebox'
import { Value } from '@sinclair/typebox/value'

// Guard for DiscriminatedUnion
function IsDiscriminatedUnion(schema: TSchema): schema is TUnion {
  return (
    // the schema is union with discriminantKey property ...
    KindGuard.IsUnion(schema) && ValueGuard.IsString(schema.discriminantKey) && 
    // ... and where union only contains object types
    schema.anyOf.every(variant => 
      // ... where each variant has a literal string property matching the discriminantKey
      KindGuard.IsObject(variant) && KindGuard.IsLiteralString(variant.properties[schema.discriminantKey])
    )
  )
}

// Override the global error function
SetErrorFunction((param) => {
  const { schema } = param
  return IsDiscriminatedUnion(schema)
    ? `Expected either ${schema.anyOf.map(schema => schema.properties.type.const).join(', ')}`
    :  DefaultErrorFunction(param)
})


// Create Union with discriminantKey
const T = Type.Union([
  Type.Object({ type: Type.Literal('A'), value: Type.Number() }),
  Type.Object({ type: Type.Literal('B'), value: Type.String() }),
  Type.Object({ type: Type.Literal('C'), value: Type.Boolean() }),
], {
  discriminantKey: 'type'
})

// Test
Value.Parse(T, null) // Error: 'Expected either A, B, C'

Additional Information on SetErrorFunction can be found below

https://github.com/sinclairzx81/typebox?tab=readme-ov-file#error-function

Will close this one out for now

Cheers!
S

@AlbertMarashi
Copy link
Author

@sinclairzx81 What I meant more so is that, when I know the type, I only wanna display the errors relevant to parsing that union type.

Right now, I just get something like Expected all values to match which isn't particularly helpful..

@Lonli-Lokli
Copy link

I must say I also have this problem with unions, is it possible to somehow split it and understand/display actual error for one case of union?

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Oct 16, 2024

It would be really nice if we could create custom types, that can extend from the built in typebox error handling

like how was kinda mentioned in

Like a user-defined type like DiscriminatedUnion, but we can some how get typebox to go through our internal code for how to parse them, with the ability for us to defer back to typebox for internal values.

So, in other words, something like:

const discriminated_union_kind = symbol("DiscriminatedUnion")
TypeBox.registerType({
  kind: discriminated_union_kind
  Parse<T extends TObject[]>(
    Schema: typeof DiscriminatedUnion<T>,
    value: unknown,
    DeferParse: ...
  ): Static<typeof DiscriminatedUnion<T> {
    let discriminant = schema.discriminant

    if (!value[discriminant]) throw new Error("Discriminant key is missing from value, expected ...")

    // inefficient example
    const Subtype = Schema.anyOf.find(sub_schema => {
        sub_schema.properties[discriminant] == value[discriminant]
    })

    if(!Subtype) throw new Error("Discriminant key ... does not exist in schema")

    // else defer to typebox's internal handling
    // where this would throw errors specific to that union's subtype
    return DeferParse(Subtype, value)
  }
  // Would we need to do this for things like Check/Clone/Assert/Convert/Create/Clean/Default?
)

@AlbertMarashi
Copy link
Author

AlbertMarashi commented Oct 16, 2024

It could even help solve issues like:

Since I could customise the parsing logic to reorder keys and implement something like OrdereredObject

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

3 participants