mirror of
https://github.com/zoriya/typebox.git
synced 2026-05-27 08:33:16 +00:00
Construct Composite Using Indexed Access Type (#420)
This commit is contained in:
Generated
+2
-2
@@ -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
@@ -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
@@ -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> {
|
||||
|
||||
@@ -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
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user