Skip to content

Commit 1cbb5c6

Browse files
authored
Composite Evaluate (#349)
1 parent 5289038 commit 1cbb5c6

File tree

9 files changed

+233
-79
lines changed

9 files changed

+233
-79
lines changed

changelog/0.26.2.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
## [0.26.2](https://www.npmjs.com/package/@sinclair/typebox/v/0.26.2)
2+
3+
Updates:
4+
5+
- [331](https://github.com/sinclairzx81/typebox/pull/349) Revert 0.25.0 Intersect logic for Composite
6+
7+
Notes:
8+
9+
This PR reverts the logic on Type.Composite back to 0.25.0 Type.Intersect due to excessive type instantiation issues. On 0.26.0, Type.Composite attempted to take a union for overlapping properties, however due to the sophistication required to type map the unions for overlapping properties, this has resulted in type instantiation problems for some users upgrading to 0.26.0.
10+
11+
As such, 0.26.2 reverts back to the 0.25.0 interpretation, but applies type mappings more inline with TS's interpretation of an overlapping varying property types. In the examples below, the type `C` is the evaluated type for Type.Composite. Note that TS will not union for overlapping properties and instead evaluate `never`. The 0.26.2 implementation falls inline with this evaluation.
12+
13+
```typescript
14+
{ // evaluation case 1: non-varying
15+
type T = { a: number } & { a: number }
16+
17+
type C = {[K in keyof T]: T[K] } // type C = { a: number }
18+
}
19+
20+
{ // evaluation case 2: varying
21+
type T = { a: number } & { a: string }
22+
23+
type C = {[K in keyof T]: T[K] } // type C = { a: never }
24+
}
25+
26+
{ // evaluation case 3: single optional
27+
type T = { a?: number } & { a: number }
28+
29+
type C = {[K in keyof T]: T[K] } // type C = { a: number }
30+
}
31+
32+
{ // evaluation case 4: all optional
33+
type T = { a?: number } & { a?: number }
34+
35+
type C = {[K in keyof T]: T[K] } // type C = { a: number | undefined }
36+
}
37+
```
38+
Note: the Type.Composite is intended to be a temporary type which can be replaced with a more general `Type.Mapped` in future revisions of TypeBox. As the infrastructure to support mapped types does not exist, users can use Type.Composite to partially replicate mapped type evaluation for composited object types only.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sinclair/typebox",
3-
"version": "0.26.1",
3+
"version": "0.26.2",
44
"description": "JSONSchema Type Builder with Static Type Resolution for TypeScript",
55
"keywords": [
66
"typescript",

readme.md

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,13 @@ The following table lists the Standard TypeBox types. These types are fully comp
229229
│ │ │ │
230230
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
231231
const T = Type.Null() │ type T = nullconst T = { │
232-
│ │ │ type: 'null'
232+
│ │ │ type: 'null'
233233
│ │ │ } │
234234
│ │ │ │
235235
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
236236
const T = Type.Literal(42) │ type T = 42const T = { │
237-
│ │ │ const: 42, │
238-
│ │ │ type: 'number'
237+
│ │ │ const: 42,
238+
│ │ │ type: 'number'
239239
│ │ │ } │
240240
│ │ │ │
241241
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
@@ -326,19 +326,16 @@ The following table lists the Standard TypeBox types. These types are fully comp
326326
│ │ │ │
327327
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
328328
const T = Type.Composite([ │ type T = { │ const T = { │
329-
│ Type.Object({ │ x: number | stringtype: 'object', │
330-
x: Type.Number() │ y: numberproperties: { │
331-
│ }), │ } │ x: { │
332-
│ Type.Object({ │ │ anyOf: [ │
333-
x: Type.String() │ │ { type: 'number' }, │
334-
y: Type.Number() │ │ { type: 'string' } │
335-
│ }) │ │ ] │
336-
│ ]) │ │ }, │
337-
│ │ │ y: { │
329+
│ Type.Object({ │ x: numbertype: 'object', │
330+
x: Type.Number() │ y: numberrequired: ['x', 'y'], │
331+
│ }), │ } │ properties: { │
332+
│ Type.Object({ │ │ x: { │
333+
y: Type.Number() │ │ type: 'number'
334+
│ }) │ │ }, │
335+
│ ]) │ │ y: { │
338336
│ │ │ type: 'number'
339337
│ │ │ } │
340-
│ │ │ }, │
341-
│ │ │ required: ['x', 'y'] │
338+
│ │ │ } │
342339
│ │ │ } │
343340
│ │ │ │
344341
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤

src/typebox.ts

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -168,21 +168,14 @@ export type TInstanceType<T extends TConstructor<TSchema[], TSchema>> = T['retur
168168
// --------------------------------------------------------------------------
169169
// TComposite
170170
// --------------------------------------------------------------------------
171-
export type TCompositeUnion<Left extends TSchema, Right extends TSchema> = Ensure<TUnion<[Left, Right]>>
172-
// note: we need to take the left and right as the accumulator is assigned for multiple composite property sets with missing properties.
173-
export type TCompositeUnionLeft<T extends TObject, Acc extends TProperties> = {
174-
[K in keyof T['properties']]: K extends keyof Acc ? TCompositeUnion<T['properties'][K], Acc[K]> : T['properties'][K]
171+
export type TCompositeEvaluateArray<T extends readonly TSchema[], P extends unknown[]> = { [K in keyof T]: T[K] extends TSchema ? Static<T[K], P> : never }
172+
export type TCompositeArray<T extends readonly TObject[]> = { [K in keyof T]: T[K] extends TObject<infer P> ? P : {} }
173+
export type TCompositeProperties<I extends unknown, T extends readonly any[]> = Evaluate<T extends [infer A, ...infer B] ? TCompositeProperties<I & A, B> : I extends object ? I : {}>
174+
export interface TComposite<T extends TObject[] = TObject[]> extends TObject {
175+
[Hint]: 'Composite' // Hint is required to differentiate between object | intersection on pick, omit, required, partial and keyof
176+
static: Evaluate<TCompositeProperties<unknown, TCompositeEvaluateArray<T, this['params']>>>
177+
properties: TCompositeProperties<unknown, TCompositeArray<T>>
175178
}
176-
export type TCompositeUnionRight<T extends TObject, Acc extends TProperties> = {
177-
[K in keyof Acc]: K extends keyof T['properties'] ? TCompositeUnion<T['properties'][K], Acc[K]> : Acc[K]
178-
}
179-
export type TCompositeUnionObject<T extends TObject, Acc extends TProperties> = Evaluate<TCompositeUnionLeft<T, Acc> & TCompositeUnionRight<T, Acc>>
180-
// prettier-ignore
181-
export type TCompositeProperties<T extends TObject[], Acc extends TProperties> =
182-
T extends [...infer L, infer R] ? TCompositeProperties<Assert<L, TObject[]>, TCompositeUnionObject<Assert<R, TObject>, Acc>> :
183-
T extends [] ? Acc :
184-
never
185-
export type TComposite<T extends TObject[] = TObject[]> = Ensure<TObject<TCompositeProperties<T, {}>>>
186179
// --------------------------------------------------------------------------
187180
// TConstructor
188181
// --------------------------------------------------------------------------
@@ -275,6 +268,7 @@ export interface IntersectOptions extends SchemaOptions {
275268
unevaluatedProperties?: TUnevaluatedProperties
276269
}
277270
export type TIntersectStatic<T extends TSchema[], P extends unknown[]> = TupleToIntersect<{ [K in keyof T]: Static<Assert<T[K], TSchema>, P> }>
271+
278272
export interface TIntersect<T extends TSchema[] = TSchema[]> extends TSchema, IntersectOptions {
279273
[Kind]: 'Intersect'
280274
type?: 'object'
@@ -292,6 +286,7 @@ export type TKeyOfTuple<T extends TSchema> = {
292286
: never
293287
// prettier-ignore
294288
export type TKeyOf<T extends TSchema = TSchema> = (
289+
T extends TComposite ? TKeyOfTuple<T> :
295290
T extends TIntersect ? TKeyOfTuple<T> :
296291
T extends TUnion ? TKeyOfTuple<T> :
297292
T extends TObject ? TKeyOfTuple<T> :
@@ -312,7 +307,7 @@ export interface TLiteral<T extends TLiteralValue = TLiteralValue> extends TSche
312307
export interface TNever extends TSchema {
313308
[Kind]: 'Never'
314309
static: never
315-
allOf: [{ type: 'boolean'; const: false }, { type: 'boolean'; const: true }]
310+
not: {}
316311
}
317312
// --------------------------------------------------------------------------
318313
// TNot
@@ -381,6 +376,7 @@ export type TOmitArray<T extends TSchema[], K extends keyof any> = Assert<{ [K2
381376
export type TOmitProperties<T extends TProperties, K extends keyof any> = Evaluate<Assert<Omit<T, K>, TProperties>>
382377
// prettier-ignore
383378
export type TOmit<T extends TSchema, K extends keyof any> =
379+
T extends TComposite<infer S> ? TComposite<TOmitArray<S, K>> :
384380
T extends TIntersect<infer S> ? TIntersect<TOmitArray<S, K>> :
385381
T extends TUnion<infer S> ? TUnion<TOmitArray<S, K>> :
386382
T extends TObject<infer S> ? TObject<TOmitProperties<S, K>> :
@@ -392,6 +388,7 @@ export type TParameters<T extends TFunction> = TTuple<T['parameters']>
392388
// --------------------------------------------------------------------------
393389
// TPartial
394390
// --------------------------------------------------------------------------
391+
export type TPartialObjectArray<T extends TObject[]> = Assert<{ [K in keyof T]: TPartial<Assert<T[K], TObject>> }, TObject[]>
395392
export type TPartialArray<T extends TSchema[]> = Assert<{ [K in keyof T]: TPartial<Assert<T[K], TSchema>> }, TSchema[]>
396393
// prettier-ignore
397394
export type TPartialProperties<T extends TProperties> = Evaluate<Assert<{
@@ -402,7 +399,8 @@ export type TPartialProperties<T extends TProperties> = Evaluate<Assert<{
402399
TOptional<T[K]>
403400
}, TProperties>>
404401
// prettier-ignore
405-
export type TPartial<T extends TSchema> =
402+
export type TPartial<T extends TSchema> =
403+
T extends TComposite<infer S> ? TComposite<TPartialArray<S>> :
406404
T extends TIntersect<infer S> ? TIntersect<TPartialArray<S>> :
407405
T extends TUnion<infer S> ? TUnion<TPartialArray<S>> :
408406
T extends TObject<infer S> ? TObject<TPartialProperties<S>> :
@@ -420,11 +418,11 @@ export type TPickProperties<T extends TProperties, K extends keyof any> =
420418
}): never
421419
// prettier-ignore
422420
export type TPick<T extends TSchema, K extends keyof any> =
421+
T extends TComposite<infer S> ? TComposite<TPickArray<S, K>> :
423422
T extends TIntersect<infer S> ? TIntersect<TPickArray<S, K>> :
424423
T extends TUnion<infer S> ? TUnion<TPickArray<S, K>> :
425424
T extends TObject<infer S> ? TObject<TPickProperties<S, K>> :
426425
T
427-
428426
// --------------------------------------------------------------------------
429427
// TPromise
430428
// --------------------------------------------------------------------------
@@ -489,6 +487,7 @@ export type TRequiredProperties<T extends TProperties> = Evaluate<Assert<{
489487
}, TProperties>>
490488
// prettier-ignore
491489
export type TRequired<T extends TSchema> =
490+
T extends TComposite<infer S> ? TComposite<TRequiredArray<S>> :
492491
T extends TIntersect<infer S> ? TIntersect<TRequiredArray<S>> :
493492
T extends TUnion<infer S> ? TUnion<TRequiredArray<S>> :
494493
T extends TObject<infer S> ? TObject<TRequiredProperties<S>> :
@@ -1845,15 +1844,41 @@ export class StandardTypeBuilder extends TypeBuilder {
18451844
public Boolean(options: SchemaOptions = {}): TBoolean {
18461845
return this.Create({ ...options, [Kind]: 'Boolean', type: 'boolean' })
18471846
}
1848-
/** `[Standard]` Creates a Composite object type that will union any overlapping properties of the given object array */
1849-
public Composite<T extends TObject[]>(schemas: [...T], options?: ObjectOptions): TComposite<T> {
1850-
const properties = {} as TProperties
1851-
for (const object of schemas) {
1852-
for (const [key, property] of globalThis.Object.entries(object.properties)) {
1853-
properties[key] = key in properties ? this.Union([properties[key], property]) : TypeClone.Clone(property, {})
1847+
/** `[Standard]` Creates a Composite object type. */
1848+
public Composite<T extends TObject[]>(objects: [...T], options?: ObjectOptions): TComposite<T> {
1849+
const isOptionalAll = (objects: TObject[], key: string) => objects.every((object) => !(key in object.properties) || IsOptional(object.properties[key]))
1850+
const IsOptional = (schema: TSchema) => TypeGuard.TOptional(schema) || TypeGuard.TReadonlyOptional(schema)
1851+
const [required, optional] = [new Set<string>(), new Set<string>()]
1852+
for (const object of objects) {
1853+
for (const key of globalThis.Object.getOwnPropertyNames(object.properties)) {
1854+
if (isOptionalAll(objects, key)) optional.add(key)
1855+
}
1856+
}
1857+
for (const object of objects) {
1858+
for (const key of globalThis.Object.getOwnPropertyNames(object.properties)) {
1859+
if (!optional.has(key)) required.add(key)
18541860
}
18551861
}
1856-
return this.Object(properties, options) as TComposite<T>
1862+
const properties = {} as Record<keyof any, any>
1863+
for (const object of objects) {
1864+
for (const [key, schema] of Object.entries(object.properties)) {
1865+
const property = TypeClone.Clone(schema, {})
1866+
if (!optional.has(key)) delete property[Modifier]
1867+
if (key in properties) {
1868+
const left = TypeExtends.Extends(properties[key], property) !== TypeExtendsResult.False
1869+
const right = TypeExtends.Extends(property, properties[key]) !== TypeExtendsResult.False
1870+
if (!left && !right) properties[key] = Type.Never()
1871+
if (!left && right) properties[key] = property
1872+
} else {
1873+
properties[key] = property
1874+
}
1875+
}
1876+
}
1877+
if (required.size > 0) {
1878+
return this.Create({ ...options, [Kind]: 'Object', [Hint]: 'Composite', type: 'object', properties, required: [...required] })
1879+
} else {
1880+
return this.Create({ ...options, [Kind]: 'Object', [Hint]: 'Composite', type: 'object', properties })
1881+
}
18571882
}
18581883
/** `[Standard]` Creates a Enum type */
18591884
public Enum<T extends Record<string, string | number>>(item: T, options: SchemaOptions = {}): TEnum<T> {

test/runtime/compiler/composite.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Type, Static } from '@sinclair/typebox'
1+
import { Type } from '@sinclair/typebox'
22
import { Ok, Fail } from './validate'
33

44
describe('type/compiler/Composite', () => {
@@ -13,33 +13,45 @@ describe('type/compiler/Composite', () => {
1313
const B = Type.Partial(Type.Object({ b: Type.Number() }))
1414
const P = Type.Composite([A, B], { additionalProperties: false })
1515
Ok(P, { a: 1, b: 2 })
16+
Ok(P, { a: 1 })
17+
Ok(P, { b: 1 })
18+
Ok(P, {})
19+
Fail(P, { a: 1, b: 2, c: 3 })
20+
Fail(P, { c: 1 })
1621
})
1722
it('Should compose with overlapping same type', () => {
1823
const A = Type.Object({ a: Type.Number() })
1924
const B = Type.Object({ a: Type.Number() })
2025
const P = Type.Composite([A, B])
2126
Ok(P, { a: 1 })
22-
Fail(P, { a: 'hello' })
23-
Fail(P, {})
27+
Fail(P, { a: '1' })
2428
})
25-
it('Should compose with overlapping varying type', () => {
29+
it('Should not compose with overlapping varying type', () => {
2630
const A = Type.Object({ a: Type.Number() })
2731
const B = Type.Object({ a: Type.String() })
2832
const T = Type.Composite([A, B])
29-
Ok(T, { a: 1 })
30-
Ok(T, { a: 'hello' })
33+
Fail(T, { a: 1 })
34+
Fail(T, { a: 'hello' })
3135
Fail(T, {})
3236
})
3337
it('Should compose with deeply nest overlapping varying type', () => {
38+
const A = Type.Object({ a: Type.Number() })
39+
const B = Type.Object({ b: Type.String() })
40+
const C = Type.Object({ c: Type.Boolean() })
41+
const D = Type.Object({ d: Type.Null() })
42+
const T = Type.Composite([A, B, C, D])
43+
Ok(T, { a: 1, b: 'hello', c: true, d: null })
44+
})
45+
it('Should not compose with deeply nest overlapping varying type', () => {
3446
const A = Type.Object({ a: Type.Number() })
3547
const B = Type.Object({ a: Type.String() })
3648
const C = Type.Object({ a: Type.Boolean() })
3749
const D = Type.Object({ a: Type.Null() })
3850
const T = Type.Composite([A, B, C, D])
39-
Ok(T, { a: 1 })
40-
Ok(T, { a: 'hello' })
41-
Ok(T, { a: false })
42-
Ok(T, { a: null })
51+
Fail(T, { a: 1 })
52+
Fail(T, { a: 'hello' })
53+
Fail(T, { a: false })
54+
Fail(T, { a: null })
4355
Fail(T, { a: [] })
4456
Fail(T, {})
4557
})
@@ -61,13 +73,12 @@ describe('type/compiler/Composite', () => {
6173
Ok(P, { x: 1, y: 1 })
6274
Fail(P, { x: 1, y: 1, z: 1 })
6375
})
64-
6576
it('Should compose nested object properties', () => {
6677
const A = Type.Object({ x: Type.Object({ x: Type.Number() }) })
67-
const B = Type.Object({ x: Type.Object({ x: Type.String() }) })
78+
const B = Type.Object({ y: Type.Object({ x: Type.String() }) })
6879
const T = Type.Composite([A, B])
69-
Ok(T, { x: { x: 1 } })
70-
Ok(T, { x: { x: 'hello' } })
71-
Fail(T, { x: { x: false } })
80+
Ok(T, { x: { x: 1 }, y: { x: '' } })
81+
Fail(T, { x: { x: '1' }, y: { x: '' } })
82+
Fail(T, { x: { x: 1 }, y: { x: 1 } })
7283
})
7384
})

0 commit comments

Comments
 (0)