diff --git a/package-lock.json b/package-lock.json index e330584..5b4dbac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.28.7", + "version": "0.28.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.28.7", + "version": "0.28.8", "license": "MIT", "devDependencies": { "@sinclair/hammer": "^0.17.1", diff --git a/package.json b/package.json index 8883728..fd300dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.28.7", + "version": "0.28.8", "description": "JSONSchema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", diff --git a/src/typebox.ts b/src/typebox.ts index 3a4a7bd..618e85a 100644 --- a/src/typebox.ts +++ b/src/typebox.ts @@ -194,34 +194,18 @@ export type TInstanceType> = T['retur // -------------------------------------------------------------------------- // TComposite // -------------------------------------------------------------------------- -export type TCompositeIsOptional = T extends TOptional | TReadonlyOptional ? true : false // prettier-ignore -export type TCompositeOptional = T extends [infer L, ...infer R] - ? TCompositeIsOptional> extends false ? false - : TCompositeOptional> : true -export type TCompositeKeyOfUnion1 = keyof T['properties'] -// prettier-ignore -export type TCompositeKeyOfUnion2 = T extends [infer L, ...infer R] - ? TCompositeKeyOfUnion1> | TCompositeKeyOfUnion2> - : never -export type TCompositeKeyOf = UnionToTuple> -export type TCompositePropertiesWithKey1 = K extends keyof T['properties'] ? [T['properties'][K]] : [] -// prettier-ignore -export type TCompositePropertiesWithKey2 = T extends [infer L, ...infer R] - ? [...TCompositePropertiesWithKey1, K>, ...TCompositePropertiesWithKey2, K>] - : [] -// prettier-ignore -export type TCompositeObjectProperty = TCompositePropertiesWithKey2 extends infer S ? - TCompositeOptional> extends true - ? { [_ in K]: TOptional>> } - : { [_ in K]: IntersectType> } +export type TCompositeReduce, K extends string[]> = K extends [infer L, ...infer R] + ? { [_ in Assert]: TIndexType> } & TCompositeReduce> : {} // prettier-ignore -export type TCompositeObjectsWithKeys = K extends [infer L, ...infer R] ? L extends Key - ? TCompositeObjectProperty & TCompositeObjectsWithKeys> - : {} +export type TCompositeSelect> = UnionToTuple> extends infer K + ? Evaluate>> : {} -export type TComposite = Ensure, Key[]>>>>> +// prettier-ignore +export type TComposite = TIntersect extends infer R + ? TObject>>> + : TObject<{}> // -------------------------------------------------------------------------- // TConstructor // -------------------------------------------------------------------------- @@ -2337,39 +2321,10 @@ export class StandardTypeBuilder extends TypeBuilder { } /** `[Standard]` Creates a Composite object type. */ public Composite(objects: [...T], options?: ObjectOptions): TComposite { - const isOptionalAll = (objects: TObject[], key: string) => objects.every((object) => !(key in object.properties) || IsOptional(object.properties[key])) - const IsOptional = (schema: TSchema) => TypeGuard.TOptional(schema) || TypeGuard.TReadonlyOptional(schema) - const [required, optional] = [new Set(), new Set()] - for (const object of objects) { - for (const key of globalThis.Object.getOwnPropertyNames(object.properties)) { - if (isOptionalAll(objects, key)) optional.add(key) - } - } - for (const object of objects) { - for (const key of globalThis.Object.getOwnPropertyNames(object.properties)) { - if (!optional.has(key)) required.add(key) - } - } - const properties = {} as Record - for (const object of objects) { - for (const [key, schema] of Object.entries(object.properties)) { - const property = TypeClone.Clone(schema, {}) - if (!optional.has(key)) delete property[Modifier] - if (key in properties) { - properties[key] = TypeGuard.TIntersect(properties[key]) ? this.Intersect([...properties[key].allOf, property]) : this.Intersect([properties[key], property]) - } else { - properties[key] = property - } - } - } - for (const key of globalThis.Object.getOwnPropertyNames(properties)) { - properties[key] = optional.has(key) ? this.Optional(properties[key]) : properties[key] - } - if (required.size > 0) { - return this.Create({ ...options, [Kind]: 'Object', type: 'object', properties, required: [...required] }) - } else { - return this.Create({ ...options, [Kind]: 'Object', type: 'object', properties }) - } + const intersect: any = Type.Intersect(objects, {}) + const keys = KeyResolver.ResolveKeys(intersect, { includePatterns: false }) + const properties = keys.reduce((acc, key) => ({ ...acc, [key]: Type.Index(intersect, [key]) }), {} as TProperties) + return Type.Object(properties, options) as TComposite } /** `[Standard]` Creates a Enum type */ public Enum>(item: T, options: SchemaOptions = {}): TEnum { diff --git a/test/runtime/type/guard/composite.ts b/test/runtime/type/guard/composite.ts index 2340577..86ced5e 100644 --- a/test/runtime/type/guard/composite.ts +++ b/test/runtime/type/guard/composite.ts @@ -20,8 +20,15 @@ describe('type/guard/TComposite', () => { const T = Type.Composite([Type.Object({ x: Type.Optional(Type.Number()) }), Type.Object({ x: Type.Number() })]) Assert.isEqual(TypeGuard.TOptional(T.properties.x), false) }) - it('Should produce optional property if all properties are optional', () => { - const T = Type.Composite([Type.Object({ x: Type.Optional(Type.Number()) }), Type.Object({ x: Type.Optional(Type.Number()) })]) - Assert.isEqual(TypeGuard.TOptional(T.properties.x), true) - }) + // Note for: https://github.com/sinclairzx81/typebox/issues/419 + // Determining if a composite property is optional requires a deep check for all properties gathered during a indexed access + // call. Currently, there isn't a trivial way to perform this check without running into possibly infinite instantiation issues. + // The optional check is only specific to overlapping properties. Singular properties will continue to work as expected. The + // rule is "if all composite properties for a key are optional, then the composite property is optional". Defer this test and + // document as minor breaking change. + // + // it('Should produce optional property if all properties are optional', () => { + // const T = Type.Composite([Type.Object({ x: Type.Optional(Type.Number()) }), Type.Object({ x: Type.Optional(Type.Number()) })]) + // Assert.isEqual(TypeGuard.TOptional(T.properties.x), true) + // }) }) diff --git a/test/static/composite.ts b/test/static/composite.ts index fcbc5c4..b4399e2 100644 --- a/test/static/composite.ts +++ b/test/static/composite.ts @@ -43,19 +43,24 @@ import { Type, Static } from '@sinclair/typebox' A: number }>() } -// Overlapping All Optional +// Overlapping All Optional (Deferred) +// Note for: https://github.com/sinclairzx81/typebox/issues/419 +// Determining if a composite property is optional requires a deep check for all properties gathered during a indexed access +// call. Currently, there isn't a trivial way to perform this check without running into possibly infinite instantiation issues. +// The optional check is only specific to overlapping properties. Singular properties will continue to work as expected. The +// rule is "if all composite properties for a key are optional, then the composite property is optional". Defer this test and +// document as minor breaking change. { - const A = Type.Object({ - A: Type.Optional(Type.Number()), - }) - const B = Type.Object({ - A: Type.Optional(Type.Number()), - }) - const T = Type.Composite([A, B]) - - Expect(T).ToInfer<{ - A: number | undefined - }>() + // const A = Type.Object({ + // A: Type.Optional(Type.Number()), + // }) + // const B = Type.Object({ + // A: Type.Optional(Type.Number()), + // }) + // const T = Type.Composite([A, B]) + // Expect(T).ToInfer<{ + // A: number | undefined + // }>() } // Distinct Properties {