sinclairzx81 / typebox

Json Schema Type Builder with Static Type Resolution for TypeScript
Other
4.56k stars 148 forks source link

`unevaluatedProperties` failed verification when combined with `Type.Intersect` and `Type.Union` #889

Open eygsoft opened 1 month ago

eygsoft commented 1 month ago

First of all, thank you very much. Recently, there is a requirement similar to { a: string } & ({ b: string } | { c: string }):

TypeBox version: 0.32.31

import { Type, type Static } from '@sinclair/typebox'
import { Value } from '@sinclair/typebox/value'
import Ajv2019 from 'ajv/dist/2019'

export type T = Static<typeof T>
const T = Type.Intersect(
  [
    Type.Object({ a: Type.String() }),
    Type.Union([
      Type.Object({ b: Type.String() }),
      Type.Object({ c: Type.String() })
    ])
  ],
  { unevaluatedProperties: false }
)
const obj: T = { a: 'a', b: 'b' }

console.log(new Ajv2019().validate(T, obj)) // true
// https://www.jsonschemavalidator.net/ Also passed
console.log(Value.Check(T, obj)) // false, "Unexpected property"

/* The generated JsonScheme itself is not a problem
{
  "unevaluatedProperties": false,
  "type": "object",
  "allOf": [
    {
      "type": "object",
      "properties": { "a": { "type": "string" } },
      "required": ["a"]
    },
    {
      "anyOf": [
        {
          "type": "object",
          "properties": { "b": { "type": "string" } },
          "required": ["b"]
        },
        {
          "type": "object",
          "properties": { "c": { "type": "string" } },
          "required": ["c"]
        }
      ]
    }
  ]
}
*/

Finally, thank you again!

sinclairzx81 commented 1 month ago

@eygsoft Hi! Thanks for reporting!

Yes, this looks like a bug. At this stage, I don't think I'll be able to take a look into the issue until the weekend at the earliest (as I am currently tied up with some other work), however you may be able to workaround this issue if you can reshape the schematics into the following.

const A = Type.Object({
 a: Type.String(),
 b: Type.String()
}, { additionalProperties: false })

const B = Type.Object({
 a: Type.String(),
 c: Type.String()
}, { additionalProperties: false })

const T = Type.Union([A, B]) // T = { a: string, b: string } | { a: string, c: string }

This issue actually crosses over with some other areas of the library where I'm looking to implement a distributive evaluate similar to the following....

TypeScript Link Here

type T = (
  { x: number }
) & (
  { y: number } |
  { z: number }
)

// non-distributive: where passing y | z is observed as unevaluated properties

type A = {[K in keyof T]: T[K]} & {} // type A = { x: number }

// distributive: where y | z are distributed with outer property x

type E<T> = {[K in keyof T]: T[K]} & {}

type B = E<T>                       // type B = { x: number; y: number; } | 
                                    //          { x: number; z: number; }

Note that TypeBox observes the validating schematic as A, where it should be observed as B. The validator should apply distributive union rules to the schematics (which has been challenging at a type level, but may be more tenable at a validation level)

Will keep you posted on any changes here. Cheers S

eygsoft commented 4 weeks ago

@eygsoft Hi! Thanks for reporting!

Yes, this looks like a bug. At this stage, I don't think I'll be able to take a look into the issue until the weekend at the earliest (as I am currently tied up with some other work), however you may be able to workaround this issue if you can reshape the schematics into the following.

const A = Type.Object({
 a: Type.String(),
 b: Type.String()
}, { additionalProperties: false })

const B = Type.Object({
 a: Type.String(),
 c: Type.String()
}, { additionalProperties: false })

const T = Type.Union([A, B]) // T = { a: string, b: string } | { a: string, c: string }

This issue actually crosses over with some other areas of the library where I'm looking to implement a distributive evaluate similar to the following....

TypeScript Link Here

type T = (
  { x: number }
) & (
  { y: number } |
  { z: number }
)

// non-distributive: where passing y | z is observed as unevaluated properties

type A = {[K in keyof T]: T[K]} & {} // type A = { x: number }

// distributive: where y | z are distributed with outer property x

type E<T> = {[K in keyof T]: T[K]} & {}

type B = E<T>                       // type B = { x: number; y: number; } | 
                                    //          { x: number; z: number; }

Note that TypeBox observes the validating schematic as A, where it should be observed as B. The validator should apply distributive union rules to the schematics (which has been challenging at a type level, but may be more tenable at a validation level)

Will keep you posted on any changes here. Cheers S

Thank you. The method I used is exactly the solution you mentioned, and it works very well. I just discovered this issue and I hope this great project can be more perfect. Thank you again!