Skip to content

Commit f35257c

Browse files
authored
Version 1.0.75 (#1507)
* Fix Union Intersect Distribution Rule * ChangeLog * Version
1 parent c3ee3b0 commit f35257c

File tree

5 files changed

+107
-55
lines changed

5 files changed

+107
-55
lines changed

changelog/1.0.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
---
44

55
### Version Updates
6+
- [Revision 1.0.75](https://github.com/sinclairzx81/typebox/pull/1507)
7+
- Fix Embedded Union Intersect Distribution Rule
68
- [Revision 1.0.74](https://github.com/sinclairzx81/typebox/pull/1505)
79
- Annual License Update
810
- [Revision 1.0.73](https://github.com/sinclairzx81/typebox/pull/1504)

src/type/engine/evaluate/distribute.ts

Lines changed: 46 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -31,83 +31,75 @@ THE SOFTWARE.
3131

3232
import { Guard } from '../../../guard/index.ts'
3333
import { type TSchema, IsSchema } from '../../types/schema.ts'
34-
import { type TIntersect, IsIntersect } from '../../types/intersect.ts'
3534
import { type TUnion, IsUnion } from '../../types/union.ts'
3635
import { type TObject, IsObject } from '../../types/object.ts'
3736
import { type TTuple, IsTuple } from '../../types/tuple.ts'
3837
import { type TComposite, Composite } from './composite.ts'
3938
import { type TNarrow, Narrow } from './narrow.ts'
40-
import { type TEvaluateType, EvaluateType } from './evaluate.ts'
4139

40+
import { type TEvaluateType, EvaluateType } from './evaluate.ts'
4241
import { type TEvaluateIntersect, EvaluateIntersect } from './evaluate.ts'
4342

44-
// -----------------------------------------------------------------------------------------
45-
// CanDistribute
46-
// -----------------------------------------------------------------------------------------
47-
type TCanDistribute<Type extends TSchema>
43+
// ------------------------------------------------------------------
44+
// IsObjectLike
45+
// ------------------------------------------------------------------
46+
type TIsObjectLike<Type extends TSchema>
4847
= Type extends TObject | TTuple ? true : false
49-
function CanDistribute<Type extends TSchema>(type: Type) {
48+
function IsObjectLike<Type extends TSchema>(type: Type) {
5049
return IsObject(type) || IsTuple(type)
5150
}
52-
// -----------------------------------------------------------------------------------------
53-
//
54-
// DistributeNormalize
55-
//
56-
// Distribute operands may be intersections, this function forces a conditional
57-
// sub-evaluation of an intersection to give either a TTObject | scalar. This
58-
// preflight mapping is used prior to running the logic for the DistributeOperation.
59-
//
60-
// -----------------------------------------------------------------------------------------
61-
type TDistributeNormalize<Type extends TSchema> =
62-
Type extends TIntersect<infer Types extends TSchema[]>
63-
? TEvaluateIntersect<Types>
64-
: Type
65-
66-
function DistributeNormalize<Type extends TSchema>(type: Type): TDistributeNormalize<Type> {
51+
// ------------------------------------------------------------------
52+
// IsUnionOperand
53+
// ------------------------------------------------------------------
54+
type TIsUnionOperand<Left extends TSchema, Right extends TSchema,
55+
IsUnionLeft extends boolean = Left extends TUnion ? true : false,
56+
IsUnionRight extends boolean = Right extends TUnion ? true : false,
57+
Result extends boolean = (
58+
[IsUnionLeft, IsUnionRight] extends [true, true] ? true :
59+
[IsUnionLeft, IsUnionRight] extends [false, true] ? true :
60+
[IsUnionLeft, IsUnionRight] extends [true, false] ? true :
61+
false
62+
)
63+
> = Result
64+
function IsUnionOperand<Left extends TSchema, Right extends TSchema>(left: Left, right: Right): TIsUnionOperand<Left, Right> {
65+
const isUnionLeft = IsUnion(left)
66+
const isUnionRight = IsUnion(right)
6767
return (
68-
IsIntersect(type)
69-
? EvaluateIntersect(type.allOf)
70-
: type
68+
isUnionLeft && isUnionRight ? true :
69+
!isUnionLeft && isUnionRight ? true :
70+
isUnionLeft && !isUnionRight ? true :
71+
false
7172
) as never
7273
}
7374
// -----------------------------------------------------------------------------------------
74-
//
7575
// DistributeOperation
76-
//
77-
// This function is crucial for type distribution and evaluation. Unlike TypeScript,
78-
// TypeBox does not distribute scalar types (e.g., numbers, strings) against object
79-
// types. When an object is encountered, the distribution shifts from scalar to
80-
// composite, with no option to revert to scalar distribution.
81-
//
82-
// This behavior is similar to TypeScript, where a number distributed against an object
83-
// is composited with the number's built-in methods. However, in TypeBox, numbers lack
84-
// methods and are treated as symbolic types. Discarding them is effectively the same
85-
// as compositing an empty `{}`. The empty object distribution for symbolic scalar
86-
// types is the rationale behind the following logic.
87-
//
8876
// -----------------------------------------------------------------------------------------
8977
type TDistributeOperation<Left extends TSchema, Right extends TSchema,
90-
NormalLeft extends TSchema = TDistributeNormalize<Left>,
91-
NormalRight extends TSchema = TDistributeNormalize<Right>,
92-
IsObjectLeft extends boolean = TCanDistribute<NormalLeft>,
93-
IsObjectRight extends boolean = TCanDistribute<NormalRight>,
78+
EvaluatedLeft extends TSchema = TEvaluateType<Left>,
79+
EvaluatedRight extends TSchema = TEvaluateType<Right>,
80+
IsUnionOperand extends boolean = TIsUnionOperand<EvaluatedLeft, EvaluatedRight>,
81+
IsObjectLeft extends boolean = TIsObjectLike<EvaluatedLeft>,
82+
IsObjectRight extends boolean = TIsObjectLike<EvaluatedRight>,
9483
Result extends TSchema = (
95-
[IsObjectLeft, IsObjectRight] extends [true, true] ? TComposite<TEvaluateType<NormalLeft>, NormalRight> :
96-
[IsObjectLeft, IsObjectRight] extends [true, false] ? TEvaluateType<NormalLeft> :
97-
[IsObjectLeft, IsObjectRight] extends [false, true] ? NormalRight :
98-
TNarrow<TEvaluateType<NormalLeft>, NormalRight>
84+
[IsUnionOperand] extends [true] ? TEvaluateIntersect<[EvaluatedLeft, EvaluatedRight]> :
85+
[IsObjectLeft, IsObjectRight] extends [true, true] ? TComposite<EvaluatedLeft, EvaluatedRight> :
86+
[IsObjectLeft, IsObjectRight] extends [true, false] ? EvaluatedLeft :
87+
[IsObjectLeft, IsObjectRight] extends [false, true] ? EvaluatedRight :
88+
TNarrow<EvaluatedLeft, EvaluatedRight>
9989
)
10090
> = Result
10191
function DistributeOperation<Left extends TSchema, Right extends TSchema>(left: Left, right: Right): TDistributeOperation<Left, Right> {
102-
const normalLeft = DistributeNormalize(left)
103-
const normalRight = DistributeNormalize(right)
104-
const isObjectLeft = CanDistribute(normalLeft)
105-
const IsObjectRight = CanDistribute(normalRight)
92+
const evaluatedLeft = EvaluateType(left)
93+
const evaluatedRight = EvaluateType(right)
94+
const isUnionOperand = IsUnionOperand(evaluatedLeft, evaluatedRight)
95+
const isObjectLeft = IsObjectLike(evaluatedLeft)
96+
const IsObjectRight = IsObjectLike(evaluatedRight)
10697
const result = (
107-
isObjectLeft && IsObjectRight ? Composite(EvaluateType(normalLeft), normalRight) :
108-
isObjectLeft && !IsObjectRight ? EvaluateType(normalLeft) :
109-
!isObjectLeft && IsObjectRight ? normalRight :
110-
Narrow(EvaluateType(normalLeft), normalRight)
98+
isUnionOperand ? EvaluateIntersect([evaluatedLeft, evaluatedRight]) :
99+
isObjectLeft && IsObjectRight ? Composite(evaluatedLeft, evaluatedRight) :
100+
isObjectLeft && !IsObjectRight ? evaluatedLeft :
101+
!isObjectLeft && IsObjectRight ? evaluatedRight :
102+
Narrow(evaluatedLeft, evaluatedRight)
111103
)
112104
return result as never
113105
}

tasks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Range } from './task/range/index.ts'
88
import { Metrics } from './task/metrics/index.ts'
99
import { Task } from 'tasksmith'
1010

11-
const Version = '1.0.74'
11+
const Version = '1.0.75'
1212

1313
// ------------------------------------------------------------------
1414
// Build

test/typebox/runtime/type/engine/action/evaluate.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,3 +825,40 @@ Test('Should Evaluate 66', () => {
825825
Assert.IsTrue(Type.IsBase(T.properties.value))
826826
Assert.IsTrue(T.properties.value instanceof Foo)
827827
})
828+
829+
// ------------------------------------------------------------------
830+
// https://github.com/sinclairzx81/typebox/issues/1506
831+
// ------------------------------------------------------------------
832+
Test('Should Evaluate 67', () => {
833+
const T = Type.Intersect([
834+
Type.Intersect([
835+
Type.Object({ c: Type.Number() }),
836+
Type.Union([
837+
Type.Object({ a: Type.Number() }),
838+
Type.Object({ b: Type.Number() })
839+
])
840+
]),
841+
Type.Object({ x: Type.Number() })
842+
])
843+
const S: Type.TUnion<[
844+
Type.TObject<{
845+
x: Type.TNumber
846+
c: Type.TNumber
847+
a: Type.TNumber
848+
}>,
849+
Type.TObject<{
850+
x: Type.TNumber
851+
c: Type.TNumber
852+
b: Type.TNumber
853+
}>
854+
]> = Type.Evaluate(T)
855+
Assert.IsTrue(Type.IsUnion(S))
856+
Assert.IsTrue(Type.IsObject(S.anyOf[0]))
857+
Assert.IsTrue(Type.IsNumber(S.anyOf[0].properties.a))
858+
Assert.IsTrue(Type.IsNumber(S.anyOf[0].properties.c))
859+
Assert.IsTrue(Type.IsNumber(S.anyOf[0].properties.x))
860+
Assert.IsTrue(Type.IsObject(S.anyOf[1]))
861+
Assert.IsTrue(Type.IsNumber(S.anyOf[1].properties.b))
862+
Assert.IsTrue(Type.IsNumber(S.anyOf[1].properties.c))
863+
Assert.IsTrue(Type.IsNumber(S.anyOf[1].properties.x))
864+
})

test/typebox/runtime/value/clean/intersect.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,3 +343,24 @@ Test('Should Clean 29', () => {
343343
const R = Value.Clean(T, { u: null, x: 1 })
344344
Assert.IsEqual(R, { x: 1 })
345345
})
346+
// ------------------------------------------------------------------
347+
// https://github.com/sinclairzx81/typebox/issues/1506
348+
// ------------------------------------------------------------------
349+
Test('Should Clean 30', () => {
350+
const T = Type.Intersect([
351+
Type.Object({ x: Type.Number() }),
352+
Type.Intersect([
353+
Type.Union([
354+
Type.Object({ a: Type.Number() }),
355+
Type.Object({ b: Type.Number() })
356+
]),
357+
Type.Object({ c: Type.Number() })
358+
])
359+
])
360+
const V1 = { a: 2, x: 1, c: 3 } // Evaluated[0]
361+
const V2 = { b: 2, x: 1, c: 3 } // Evaluated[1]
362+
const R1 = Value.Clean(T, V1)
363+
const R2 = Value.Clean(T, V2)
364+
Assert.IsEqual(V1, R1)
365+
Assert.IsEqual(V2, R2)
366+
})

0 commit comments

Comments
 (0)