Construct Composite Using Indexed Access Type (#420)

This commit is contained in:
sinclairzx81
2023-04-28 18:17:18 +09:00
committed by GitHub
parent 11b5e1cbf9
commit a4f412b63c
5 changed files with 43 additions and 76 deletions
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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",
+12 -57
View File
@@ -194,34 +194,18 @@ export type TInstanceType<T extends TConstructor<TSchema[], TSchema>> = T['retur
// --------------------------------------------------------------------------
// TComposite
// --------------------------------------------------------------------------
export type TCompositeIsOptional<T extends TSchema> = T extends TOptional<T> | TReadonlyOptional<T> ? true : false
// prettier-ignore
export type TCompositeOptional<T extends TSchema[]> = T extends [infer L, ...infer R]
? TCompositeIsOptional<AssertType<L>> extends false ? false
: TCompositeOptional<AssertRest<R>> : true
export type TCompositeKeyOfUnion1<T extends TObject> = keyof T['properties']
// prettier-ignore
export type TCompositeKeyOfUnion2<T extends TObject[]> = T extends [infer L, ...infer R]
? TCompositeKeyOfUnion1<Assert<L, TObject>> | TCompositeKeyOfUnion2<Assert<R, TObject[]>>
: never
export type TCompositeKeyOf<T extends TObject[]> = UnionToTuple<TCompositeKeyOfUnion2<T>>
export type TCompositePropertiesWithKey1<T extends TObject, K extends Key> = K extends keyof T['properties'] ? [T['properties'][K]] : []
// prettier-ignore
export type TCompositePropertiesWithKey2<T extends TObject[], K extends Key> = T extends [infer L, ...infer R]
? [...TCompositePropertiesWithKey1<Assert<L, TObject>, K>, ...TCompositePropertiesWithKey2<Assert<R, TObject[]>, K>]
: []
// prettier-ignore
export type TCompositeObjectProperty<T extends TObject[], K extends Key> = TCompositePropertiesWithKey2<T, K> extends infer S ?
TCompositeOptional<AssertRest<S>> extends true
? { [_ in K]: TOptional<IntersectType<AssertRest<S>>> }
: { [_ in K]: IntersectType<AssertRest<S>> }
export type TCompositeReduce<T extends TIntersect<TObject[]>, K extends string[]> = K extends [infer L, ...infer R]
? { [_ in Assert<L, string>]: TIndexType<T, Assert<L, string>> } & TCompositeReduce<T, Assert<R, string[]>>
: {}
// prettier-ignore
export type TCompositeObjectsWithKeys<T extends TObject[], K extends Key[]> = K extends [infer L, ...infer R] ? L extends Key
? TCompositeObjectProperty<T, L> & TCompositeObjectsWithKeys<T, Assert<R, Key[]>>
: {}
export type TCompositeSelect<T extends TIntersect<TObject[]>> = UnionToTuple<keyof Static<T>> extends infer K
? Evaluate<TCompositeReduce<T, Assert<K, string[]>>>
: {}
export type TComposite<T extends TObject[]> = Ensure<TObject<Evaluate<TCompositeObjectsWithKeys<T, Assert<TCompositeKeyOf<T>, Key[]>>>>>
// prettier-ignore
export type TComposite<T extends TObject[]> = TIntersect<T> extends infer R
? TObject<TCompositeSelect<Assert<R, TIntersect<TObject[]>>>>
: TObject<{}>
// --------------------------------------------------------------------------
// TConstructor
// --------------------------------------------------------------------------
@@ -2337,39 +2321,10 @@ export class StandardTypeBuilder extends TypeBuilder {
}
/** `[Standard]` Creates a Composite object type. */
public Composite<T extends TObject[]>(objects: [...T], options?: ObjectOptions): TComposite<T> {
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<string>(), new Set<string>()]
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<keyof any, any>
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<T>
}
/** `[Standard]` Creates a Enum type */
public Enum<T extends Record<string, string | number>>(item: T, options: SchemaOptions = {}): TEnum<T> {
+11 -4
View File
@@ -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)
// })
})
+17 -12
View File
@@ -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
{