diff --git a/changelog/1.4.0.md b/changelog/1.4.0.md index 52c8cddfe..437c95b65 100644 --- a/changelog/1.4.0.md +++ b/changelog/1.4.0.md @@ -3,6 +3,8 @@ --- ### Version Updates +- [Revision 1.4.2](https://github.com/sinclairzx81/typebox/pull/1719) + - Ensure Pick and Omit Retain Property Modifiers - [Revision 1.4.1](https://github.com/sinclairzx81/typebox/pull/1717) - Faster FNV1A-64 Hashing [#1713](https://github.com/sinclairzx81/typebox/pull/1713) diff --git a/src/type/engine/evaluate/composite.ts b/src/type/engine/evaluate/composite.ts index ab756c443..9b2b5e770 100644 --- a/src/type/engine/evaluate/composite.ts +++ b/src/type/engine/evaluate/composite.ts @@ -31,82 +31,15 @@ THE SOFTWARE. import { Guard } from '../../../guard/index.ts' -import { type TReadonly, IsReadonly } from '../../types/_readonly.ts' -import { type TOptional, IsOptional } from '../../types/_optional.ts' - import { type TSchema } from '../../types/schema.ts' import { type TProperties } from '../../types/properties.ts' import { type TObject, Object, IsObject } from '../../types/object.ts' import { type TNever, Never } from '../../types/never.ts' import { type TTuple, IsTuple } from '../../types/tuple.ts' -import { type TAddReadonly, AddReadonly } from '../../action/_add_readonly.ts' -import { type TAddOptional, AddOptional } from '../../action/_add_optional.ts' -import { type TRemoveReadonly, RemoveReadonly } from '../../action/_remove_readonly.ts' -import { type TRemoveOptional, RemoveOptional } from '../../action/_remove_optional.ts' - import { type TTupleElementsToProperties, TupleElementsToProperties } from '../tuple/to_object.ts' -import { type TEvaluateIntersect, EvaluateIntersect } from './evaluate.ts' +import { type TEvaluatePropertyIntersect, EvaluatePropertyIntersect } from './property.ts' -// ------------------------------------------------------------------ -// CanComposite (note: TWith requires infer here. review) -// ------------------------------------------------------------------ -/** Returns true if the type is a valid operand to Composite. */ -export type TCanComposite = ( - Type extends TObject ? true : - Type extends TTuple ? true : - false -) -/** Returns true if the type is a valid operand to Composite. */ -export function CanComposite(type: Type): TCanComposite { - return (IsObject(type) || IsTuple(type)) as never -} -// ---------------------------------------------------------------------------- -// IsReadonlyProperty -// ---------------------------------------------------------------------------- -type TIsReadonlyProperty = ( - Left extends TReadonly ? Right extends TReadonly ? true : false : false -) -function IsReadonlyProperty(left: Left, right: Right): TIsReadonlyProperty { - return (IsReadonly(left) ? IsReadonly(right) ? true : false : false) as never -} -// ---------------------------------------------------------------------------- -// IsOptionalProperty -// ---------------------------------------------------------------------------- -type TIsOptionalProperty = ( - Left extends TOptional ? Right extends TOptional ? true : false : false -) -function IsOptionalProperty(left: Left, right: Right): TIsOptionalProperty { - return (IsOptional(left) ? IsOptional(right) ? true : false : false) as never -} -// ---------------------------------------------------------------------------- -// CompositeProperty -// ---------------------------------------------------------------------------- -type TCompositeProperty, - IsOptional extends boolean = TIsOptionalProperty, - Evaluated extends TSchema = TEvaluateIntersect<[Left, Right]>, - // Modifiers need to be discarded and re-applied - Property extends TSchema = TRemoveReadonly> -> = ( - [IsReadonly, IsOptional] extends [true, true] ? TAddReadonly> : - [IsReadonly, IsOptional] extends [true, false] ? TAddReadonly : - [IsReadonly, IsOptional] extends [false, true] ? TAddOptional : - Property -) -function CompositeProperty(left: Left, right: Right): TCompositeProperty { - const isReadonly = IsReadonlyProperty(left, right) - const isOptional = IsOptionalProperty(left, right) - const evaluated = EvaluateIntersect([left, right]) as TSchema - // Modifiers need to be discarded and re-applied - const property = RemoveReadonly(RemoveOptional(evaluated)) - return ( - isReadonly && isOptional ? AddReadonly(AddOptional(property)) : - isReadonly && !isOptional ? AddReadonly(property) : - !isReadonly && isOptional ? AddOptional(property) : - property - ) as never -} // ---------------------------------------------------------------------------- // CompositePropertyKey // @@ -119,14 +52,14 @@ function CompositeProperty(left: Le type TCompositePropertyKey : Left[Key] + ? Key extends keyof Right ? TEvaluatePropertyIntersect : Left[Key] : Key extends keyof Right ? Right[Key] : TNever ) > = Result function CompositePropertyKey(left: Left, right: Right, key: Key): TCompositePropertyKey { return ( key in left - ? key in right ? CompositeProperty(left[key], right[key]) : left[key] + ? key in right ? EvaluatePropertyIntersect(left[key], right[key]) : left[key] : key in right ? right[key] : Never() ) as never } diff --git a/src/type/engine/evaluate/index.ts b/src/type/engine/evaluate/index.ts index cbcadba9b..bc054ba76 100644 --- a/src/type/engine/evaluate/index.ts +++ b/src/type/engine/evaluate/index.ts @@ -34,3 +34,4 @@ export * from './evaluate.ts' export * from './flatten.ts' export * from './instantiate.ts' export * from './narrow.ts' +export * from './property.ts' diff --git a/src/type/engine/evaluate/narrow.ts b/src/type/engine/evaluate/narrow.ts index ca264af72..7615585cc 100644 --- a/src/type/engine/evaluate/narrow.ts +++ b/src/type/engine/evaluate/narrow.ts @@ -30,11 +30,14 @@ THE SOFTWARE. import { Guard } from '../../../guard/index.ts' import { type TSchema } from '../../types/schema.ts' +import { type TProperties } from '../../types/properties.ts' +import { type TObject, IsObject } from '../../types/object.ts' import { type TAny, IsAny } from '../../types/any.ts' import { type TNever, Never, IsNever } from '../../types/never.ts' +import { type TTuple, IsTuple } from '../../types/tuple.ts' import { type TUnknown, IsUnknown } from '../../types/unknown.ts' import { type TCompare, type TCompareResult, Compare, CompareResultLeftInside, CompareResultRightInside, CompareResultEqual } from './compare.ts' -import { type TComposite, type TCanComposite, Composite, CanComposite } from './composite.ts' +import { type TComposite, Composite } from './composite.ts' // ------------------------------------------------------------------ // NarrowCompareRule @@ -57,6 +60,17 @@ function NarrowCompareRule(left: Le ) as never } // ------------------------------------------------------------------ +// CanComposite +// ------------------------------------------------------------------ +type TCanComposite = ( + Type extends TObject ? true : + Type extends TTuple ? true : + false +) +function CanComposite(type: Type): TCanComposite { + return (IsObject(type) || IsTuple(type)) as never +} +// ------------------------------------------------------------------ // NarrowCompositeRule // ------------------------------------------------------------------ type TNarrowCompositeRule, + Property extends TSchema = TRemoveReadonlyAction> +> = ( + [IsReadonly, IsOptional] extends [true, true] ? TAddReadonlyAction> : + [IsReadonly, IsOptional] extends [true, false] ? TAddReadonlyAction : + [IsReadonly, IsOptional] extends [false, true] ? TAddOptionalAction : + Property +) +export function EvaluatePropertyIntersect(left: Left, right: Right): TEvaluatePropertyIntersect { + const isReadonly = IsReadonly(left) && IsReadonly(right) + const isOptional = IsOptional(left) && IsOptional(right) + const evaluated = EvaluateIntersect([left, right]) as TSchema + const property = RemoveReadonlyAction(RemoveOptionalAction(evaluated, {}), {}) + return ( + isReadonly && isOptional ? AddReadonlyAction(AddOptionalAction(property, {}), {}) : + isReadonly ? AddReadonlyAction(property, {}) : + isOptional ? AddOptionalAction(property, {}) : + property + ) as never +} +// ------------------------------------------------------------------ +// EvaluatePropertyUnion +// +// Evaluates Left and Right as a union after stripping modifiers. +// It reapplies Optional and Readonly if either operand has them. +// ------------------------------------------------------------------ +export type TEvaluatePropertyUnion>, + RightProperty extends TSchema = TRemoveReadonlyAction>, + Property extends TSchema = TEvaluateUnion<[LeftProperty, RightProperty]> +> = ( + [IsReadonly, IsOptional] extends [true, true] ? TAddReadonlyAction> : + [IsReadonly, IsOptional] extends [true, false] ? TAddReadonlyAction : + [IsReadonly, IsOptional] extends [false, true] ? TAddOptionalAction : + Property +) +export function EvaluatePropertyUnion(left: Left, right: Right): TEvaluatePropertyUnion { + const isReadonly = IsReadonly(left) || IsReadonly(right) + const isOptional = IsOptional(left) || IsOptional(right) + const leftProperty = RemoveReadonlyAction(RemoveOptionalAction(left, {}), {}) + const rightProperty = RemoveReadonlyAction(RemoveOptionalAction(right, {}), {}) + const property = EvaluateUnion([leftProperty, rightProperty]) + return ( + isReadonly && isOptional ? AddReadonlyAction(AddOptionalAction(property, {}), {}) : + isReadonly ? AddReadonlyAction(property, {}) : + isOptional ? AddOptionalAction(property, {}) : + property + ) as never +} diff --git a/src/type/engine/object/from_intersect.ts b/src/type/engine/object/from_intersect.ts index 4edb8aaed..c9dcbba78 100644 --- a/src/type/engine/object/from_intersect.ts +++ b/src/type/engine/object/from_intersect.ts @@ -34,7 +34,7 @@ import { Guard } from '../../../guard/index.ts' import { type TSchema } from '../../types/schema.ts' import { type TProperties } from '../../types/properties.ts' import { type TFromType, FromType } from './from_type.ts' -import { type TEvaluateIntersect, EvaluateIntersect } from '../evaluate/evaluate.ts' +import { type TEvaluatePropertyIntersect, EvaluatePropertyIntersect } from '../evaluate/property.ts' // ------------------------------------------------------------------ // CollapseProperties @@ -46,7 +46,7 @@ type TCollapseIntersectProperties }, + SharedProperties extends TProperties = { [Key in SharedKeys]: TEvaluatePropertyIntersect }, Unique extends TProperties = Memory.TAssign, Shared extends TProperties = Memory.TAssign @@ -59,7 +59,7 @@ function CollapseIntersectProperties ({ ...result, [key]: left[key] }), {}) const rightProperties = rightKeys.reduce((result, key) => ({ ...result, [key]: right[key] }), {}) - const sharedProperties = sharedKeys.reduce((result, key) => ({ ...result, [key]: EvaluateIntersect([left[key], right[key]]) }), {}) + const sharedProperties = sharedKeys.reduce((result, key) => ({ ...result, [key]: EvaluatePropertyIntersect(left[key], right[key]) }), {}) const unique = Memory.Assign(leftProperties, rightProperties) const shared = Memory.Assign(unique, sharedProperties) diff --git a/src/type/engine/object/from_union.ts b/src/type/engine/object/from_union.ts index 1acdf9342..8cc18a0ae 100644 --- a/src/type/engine/object/from_union.ts +++ b/src/type/engine/object/from_union.ts @@ -32,7 +32,7 @@ import { Guard } from '../../../guard/index.ts' import { type TUnreachable, Unreachable } from '../../../system/unreachable/index.ts' import { type TSchema } from '../../types/schema.ts' import { type TProperties } from '../../types/properties.ts' -import { type TEvaluateUnion, EvaluateUnion } from '../evaluate/evaluate.ts' +import { type TEvaluatePropertyUnion, EvaluatePropertyUnion } from '../evaluate/property.ts' import { type TFromType, FromType } from './from_type.ts' // ------------------------------------------------------------------ @@ -41,7 +41,7 @@ import { type TFromType, FromType } from './from_type.ts' type TCollapseUnionProperties + [Key in SharedKeys]: TEvaluatePropertyUnion } > = Result function CollapseUnionProperties @@ -49,7 +49,7 @@ function CollapseUnionProperties { const sharedKeys = Guard.Keys(left).filter((key) => key in right) const result = sharedKeys.reduce((result, key) => { - return { ...result, [key]: EvaluateUnion([left[key], right[key]]) } + return { ...result, [key]: EvaluatePropertyUnion(left[key], right[key]) } }, {}) as never return result as never } diff --git a/tasks.ts b/tasks.ts index 76f3e2b08..23c6092cb 100644 --- a/tasks.ts +++ b/tasks.ts @@ -13,7 +13,7 @@ import * as Test from './task/test/index.ts' import * as Spec from './task/spec/index.ts' import * as Task from './task/task.ts' -const Version = '1.4.1' +const Version = '1.4.2' // ------------------------------------------------------------------ // BuildTo diff --git a/test/typebox/runtime/type/engine/action/indexed.ts b/test/typebox/runtime/type/engine/action/indexed.ts index c21270c5c..063cb2f94 100644 --- a/test/typebox/runtime/type/engine/action/indexed.ts +++ b/test/typebox/runtime/type/engine/action/indexed.ts @@ -607,3 +607,91 @@ Test('Should Index 44', () => { const T: Type.TNumber = Type.Index(L, R) Assert.IsTrue(Type.IsNumber(T)) }) +// ------------------------------------------------------------------ +// Modifier Coverage +// ------------------------------------------------------------------ +Test('Should Index 45', () => { + const A = Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())) + }) + const T: Type.TReadonly> = Type.Index(A, Type.Literal('x')) + Assert.IsTrue(Type.IsNumber(T)) + Assert.IsTrue(Type.IsOptional(T)) + Assert.IsTrue(Type.IsReadonly(T)) +}) +Test('Should Index 46', () => { + const A = Type.Object({ + x: Type.Readonly(Type.Number()) + }) + const T: Type.TReadonly = Type.Index(A, Type.Literal('x')) + Assert.IsTrue(Type.IsNumber(T)) + Assert.IsFalse(Type.IsOptional(T)) + Assert.IsTrue(Type.IsReadonly(T)) +}) +// Future: Here we select x and y properties, producing a Union; +// however, the variants contain the embedded modifiers when we would +// have expected the variants to be modifier-unwrapped and applied +// to the outer Union. There is some uncertainty as to how best to +// handle this case, so for now we just select the properties +// and modifiers verbatim. (review) +Test('Should Index 47', () => { + const A = Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + y: Type.Readonly(Type.String()) + }) + const T: Type.TUnion<[ + Type.TReadonly>, + Type.TReadonly + ]> = Type.Index( + A, + Type.Union([ + Type.Literal('x'), + Type.Literal('y') + ]) + ) + Assert.IsTrue(Type.IsUnion(T)) + Assert.IsTrue(Type.IsOptional(T.anyOf[0])) + Assert.IsTrue(Type.IsReadonly(T.anyOf[0])) + Assert.IsFalse(Type.IsOptional(T.anyOf[1])) + Assert.IsTrue(Type.IsReadonly(T.anyOf[1])) +}) +Test('Should Index 48', () => { + const A = Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))) + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))) + }) + ]) + const T: Type.TReadonly< + Type.TOptional< + Type.TObject<{ + a: Type.TNumber + b: Type.TNumber + }> + > + > = Type.Index(A, Type.Literal('x')) + Assert.IsTrue(Type.IsObject(T)) + Assert.IsTrue(Type.IsOptional(T)) + Assert.IsTrue(Type.IsReadonly(T)) + Assert.IsTrue(Type.IsNumber(T.properties.a)) + Assert.IsTrue(Type.IsNumber(T.properties.b)) + Assert.IsEqual(T.required, ['a', 'b']) +}) +Test('Should Index 49', () => { + const A = Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())) + }), + Type.Object({ + x: Type.String() + }) + ]) + const T: Type.TUnion<[Type.TNumber, Type.TString]> = Type.Index(A, Type.Literal('x')) + Assert.IsTrue(Type.IsUnion(T)) + Assert.IsFalse(Type.IsOptional(T)) + Assert.IsFalse(Type.IsReadonly(T)) + Assert.IsTrue(Type.IsNumber(T.anyOf[0])) + Assert.IsTrue(Type.IsString(T.anyOf[1])) +}) diff --git a/test/typebox/runtime/type/engine/action/mapped.ts b/test/typebox/runtime/type/engine/action/mapped.ts index e5217df7c..e47814384 100644 --- a/test/typebox/runtime/type/engine/action/mapped.ts +++ b/test/typebox/runtime/type/engine/action/mapped.ts @@ -179,3 +179,40 @@ Test('Should Mapped 12', () => { Assert.IsTrue(Type.IsNumber(T.properties[0])) Assert.IsTrue(Type.IsString(T.properties[1])) }) +// ------------------------------------------------------------------ +// Retain Modifiers +// +// Ref: https://github.com/sinclairzx81/typebox/issues/1718 +// ------------------------------------------------------------------ +Test('Should Mapped 13', () => { + const A = Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))) + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))) + }) + ]) + const K = Type.Identifier('K') + const B = Type.KeyOf(A) + const C = Type.Ref('K') + const D = Type.Index(A, Type.Ref('K')) + const T: Type.TObject<{ + x: Type.TReadonly< + Type.TOptional< + Type.TObject<{ + a: Type.TNumber + b: Type.TNumber + }> + > + > + }> = Type.Mapped(K, B, C, D) + Assert.IsTrue(Type.IsObject(T)) + Assert.IsTrue(Type.IsReadonly(T.properties.x)) + Assert.IsTrue(Type.IsOptional(T.properties.x)) + Assert.IsTrue(Type.IsObject(T.properties.x)) + Assert.IsTrue(Type.IsNumber(T.properties.x.properties.a)) + Assert.IsTrue(Type.IsNumber(T.properties.x.properties.b)) + Assert.IsEqual(T.properties.x.required, ['a', 'b']) + Assert.IsEqual(T.required, undefined) +}) diff --git a/test/typebox/runtime/type/engine/action/omit.ts b/test/typebox/runtime/type/engine/action/omit.ts index cac8b3a4b..94492c55a 100644 --- a/test/typebox/runtime/type/engine/action/omit.ts +++ b/test/typebox/runtime/type/engine/action/omit.ts @@ -502,3 +502,196 @@ Test('Should Omit 31', () => { Assert.IsTrue(Type.IsString(T.properties.y)) Assert.IsEqual(Guard.Keys(T.properties).length, 2) }) +// ------------------------------------------------------------------ +// Retain Modifiers +// +// Ref: https://github.com/sinclairzx81/typebox/issues/1718 +// ------------------------------------------------------------------ +Test('Should Omit 32', () => { + const T: Type.TObject<{ + x: Type.TReadonly< + Type.TOptional< + Type.TObject<{ + a: Type.TNumber + b: Type.TNumber + }> + > + > + }> = Type.Omit( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('z') + ) + Assert.IsTrue(Type.IsObject(T)) + Assert.IsEqual(T.required, undefined) + Assert.IsTrue(Type.IsOptional(T.properties.x)) + Assert.IsTrue(Type.IsReadonly(T.properties.x)) + Assert.IsEqual(Guard.Keys(T.properties), ['x']) + Assert.IsTrue(Type.IsObject(T.properties.x)) + Assert.IsEqual(Guard.Keys(T.properties.x.properties), ['a', 'b']) + Assert.IsEqual(T.properties.x.required, ['a', 'b']) + Assert.IsTrue(Type.IsNumber(T.properties.x.properties.a)) + Assert.IsTrue(Type.IsNumber(T.properties.x.properties.b)) +}) +Test('Should Omit 33', () => { + const U: Type.TObject<{ + x: Type.TReadonly< + Type.TOptional< + Type.TObject<{ + a: Type.TNumber + b: Type.TNumber + }> + > + > + z: Type.TNumber + }> = Type.Omit( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('c') + ) + Assert.IsTrue(Type.IsObject(U)) + Assert.IsEqual(U.required, ['z']) + Assert.IsTrue(Type.IsOptional(U.properties.x)) + Assert.IsTrue(Type.IsReadonly(U.properties.x)) + Assert.IsEqual(Guard.Keys(U.properties), ['x', 'z']) + Assert.IsTrue(Type.IsObject(U.properties.x)) + Assert.IsEqual(Guard.Keys(U.properties.x.properties), ['a', 'b']) + Assert.IsEqual(U.properties.x.required, ['a', 'b']) + Assert.IsTrue(Type.IsNumber(U.properties.x.properties.a)) + Assert.IsTrue(Type.IsNumber(U.properties.x.properties.b)) + Assert.IsTrue(Type.IsNumber(U.properties.z)) +}) +Test('Should Omit 34', () => { + const N: Type.TObject<{ + x: Type.TReadonly< + Type.TOptional< + Type.TObject<{ + a: Type.TNumber + b: Type.TNumber + }> + > + > + z: Type.TNumber + }> = Type.Omit( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Never() + ) + Assert.IsTrue(Type.IsObject(N)) + Assert.IsEqual(N.required, ['z']) + Assert.IsTrue(Type.IsOptional(N.properties.x)) + Assert.IsTrue(Type.IsReadonly(N.properties.x)) + Assert.IsEqual(Guard.Keys(N.properties), ['x', 'z']) + Assert.IsTrue(Type.IsObject(N.properties.x)) + Assert.IsEqual(Guard.Keys(N.properties.x.properties), ['a', 'b']) + Assert.IsEqual(N.properties.x.required, ['a', 'b']) + Assert.IsTrue(Type.IsNumber(N.properties.x.properties.a)) + Assert.IsTrue(Type.IsNumber(N.properties.x.properties.b)) + Assert.IsTrue(Type.IsNumber(N.properties.z)) +}) +Test('Should Omit 35', () => { + const T: Type.TObject<{ + x: Type.TReadonly>> + }> = Type.Omit( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('z') + ) + Assert.IsTrue(Type.IsObject(T)) + Assert.IsEqual(T.required, undefined) + Assert.IsTrue(Type.IsOptional(T.properties.x)) + Assert.IsTrue(Type.IsReadonly(T.properties.x)) + Assert.IsEqual(Guard.Keys(T.properties), ['x']) + Assert.IsTrue(Type.IsUnion(T.properties.x)) + Assert.IsEqual(T.properties.x.anyOf.length, 2) + Assert.IsTrue(Type.IsNumber(T.properties.x.anyOf[0])) + Assert.IsTrue(Type.IsString(T.properties.x.anyOf[1])) +}) +Test('Should Omit 36', () => { + const V: Type.TObject<{ + x: Type.TReadonly>> + z: Type.TNumber + }> = Type.Omit( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('c') + ) + Assert.IsTrue(Type.IsObject(V)) + Assert.IsEqual(V.required, ['z']) + Assert.IsTrue(Type.IsOptional(V.properties.x)) + Assert.IsTrue(Type.IsReadonly(V.properties.x)) + Assert.IsEqual(Guard.Keys(V.properties), ['x', 'z']) + Assert.IsTrue(Type.IsUnion(V.properties.x)) + Assert.IsEqual(V.properties.x.anyOf.length, 2) + Assert.IsTrue(Type.IsNumber(V.properties.x.anyOf[0])) + Assert.IsTrue(Type.IsString(V.properties.x.anyOf[1])) + Assert.IsTrue(Type.IsNumber(V.properties.z)) +}) +Test('Should Omit 37', () => { + const N: Type.TObject<{ + x: Type.TReadonly>> + z: Type.TNumber + }> = Type.Omit( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Never() + ) + Assert.IsTrue(Type.IsObject(N)) + Assert.IsEqual(N.required, ['z']) + Assert.IsTrue(Type.IsOptional(N.properties.x)) + Assert.IsTrue(Type.IsReadonly(N.properties.x)) + Assert.IsEqual(Guard.Keys(N.properties), ['x', 'z']) + Assert.IsTrue(Type.IsUnion(N.properties.x)) + Assert.IsEqual(N.properties.x.anyOf.length, 2) + Assert.IsTrue(Type.IsNumber(N.properties.x.anyOf[0])) + Assert.IsTrue(Type.IsString(N.properties.x.anyOf[1])) + Assert.IsTrue(Type.IsNumber(N.properties.z)) +}) diff --git a/test/typebox/runtime/type/engine/action/pick.ts b/test/typebox/runtime/type/engine/action/pick.ts index ed05fe33c..d0d716a35 100644 --- a/test/typebox/runtime/type/engine/action/pick.ts +++ b/test/typebox/runtime/type/engine/action/pick.ts @@ -522,3 +522,136 @@ Test('Should Pick 30', () => { Assert.IsEqual(T.properties.z.const, 1) Assert.IsEqual(Guard.Keys(T.properties).length, 1) }) +// ------------------------------------------------------------------ +// Retain Modifiers +// +// Ref: https://github.com/sinclairzx81/typebox/issues/1718 +// ------------------------------------------------------------------ +Test('Should Pick 31', () => { + const T: Type.TObject<{ + x: Type.TReadonly< + Type.TOptional< + Type.TObject<{ + a: Type.TNumber + b: Type.TNumber + }> + > + > + }> = Type.Pick( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('x') + ) + Assert.IsTrue(Type.IsObject(T)) + Assert.IsEqual(T.required, undefined) + Assert.IsTrue(Type.IsOptional(T.properties.x)) + Assert.IsTrue(Type.IsReadonly(T.properties.x)) + Assert.IsEqual(Guard.Keys(T.properties), ['x']) + Assert.IsTrue(Type.IsObject(T.properties.x)) + Assert.IsEqual(Guard.Keys(T.properties.x.properties), ['a', 'b']) + Assert.IsEqual(T.properties.x.required, ['a', 'b']) + Assert.IsTrue(Type.IsNumber(T.properties.x.properties.a)) + Assert.IsTrue(Type.IsNumber(T.properties.x.properties.b)) +}) +Test('Should Pick 32', () => { + const U: Type.TObject<{}> = Type.Pick( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('c') + ) + Assert.IsEqual(U.required, undefined) + Assert.IsEqual(Guard.Keys(U.properties), []) +}) +Test('Should Pick 33', () => { + const N: Type.TObject<{}> = Type.Pick( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Never() + ) + Assert.IsEqual(N.required, undefined) + Assert.IsEqual(Guard.Keys(N.properties), []) +}) +Test('Should Pick 34', () => { + const T: Type.TObject<{ + x: Type.TReadonly>> + }> = Type.Pick( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('x') + ) + Assert.IsTrue(Type.IsObject(T)) + Assert.IsEqual(T.required, undefined) + Assert.IsTrue(Type.IsOptional(T.properties.x)) + Assert.IsTrue(Type.IsReadonly(T.properties.x)) + Assert.IsEqual(Guard.Keys(T.properties), ['x']) + Assert.IsTrue(Type.IsUnion(T.properties.x)) + Assert.IsEqual(T.properties.x.anyOf.length, 2) + Assert.IsTrue(Type.IsNumber(T.properties.x.anyOf[0])) + Assert.IsTrue(Type.IsString(T.properties.x.anyOf[1])) +}) +Test('Should Pick 35', () => { + const V: Type.TObject<{}> = Type.Pick( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('c') + ) + Assert.IsEqual(V.required, undefined) + Assert.IsEqual(Guard.Keys(V.properties), []) +}) +Test('Should Pick 36', () => { + const N: Type.TObject<{}> = Type.Pick( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Never() + ) + Assert.IsEqual(N.required, undefined) + Assert.IsEqual(Guard.Keys(N.properties), []) +}) diff --git a/test/typebox/runtime/type/engine/action/~property.ts b/test/typebox/runtime/type/engine/action/~property.ts new file mode 100644 index 000000000..3f6b2e56c --- /dev/null +++ b/test/typebox/runtime/type/engine/action/~property.ts @@ -0,0 +1,91 @@ +import { Assert } from 'test' +import * as Type from 'typebox' + +const Test = Assert.Context('Type.Engine.EvaluateProperty') + +// ------------------------------------------------------------------ +// EvaluatePropertyIntersect +// ------------------------------------------------------------------ +Test('Should EvaluatePropertyIntersect 1', () => { + const A = Type.Readonly(Type.Optional(Type.Number())) + const B = Type.Readonly(Type.Number()) + const T: Type.TReadonly = Type.EvaluatePropertyIntersect(A, B) + Assert.IsTrue(Type.IsNumber(T)) + Assert.IsTrue(Type.IsReadonly(T)) + Assert.IsFalse(Type.IsOptional(T)) +}) +Test('Should EvaluatePropertyIntersect 2', () => { + const A = Type.Readonly(Type.Optional(Type.Number())) + const B = Type.Optional(Type.Number()) + const T: Type.TOptional = Type.EvaluatePropertyIntersect(A, B) + Assert.IsTrue(Type.IsNumber(T)) + Assert.IsFalse(Type.IsReadonly(T)) + Assert.IsTrue(Type.IsOptional(T)) +}) +Test('Should EvaluatePropertyIntersect 3', () => { + const A = Type.Readonly(Type.Optional(Type.Number())) + const B = Type.Number() + const T: Type.TNumber = Type.EvaluatePropertyIntersect(A, B) + Assert.IsTrue(Type.IsNumber(T)) + Assert.IsFalse(Type.IsReadonly(T)) + Assert.IsFalse(Type.IsOptional(T)) +}) +Test('Should EvaluatePropertyIntersect 4', () => { + const A = Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))) + const B = Type.Readonly(Type.Optional(Type.Object({ b: Type.String() }))) + const T: Type.TReadonly< + Type.TOptional< + Type.TObject<{ + a: Type.TNumber + b: Type.TString + }> + > + > = Type.EvaluatePropertyIntersect(A, B) + Assert.IsTrue(Type.IsObject(T)) + Assert.IsTrue(Type.IsReadonly(T)) + Assert.IsTrue(Type.IsOptional(T)) + Assert.IsTrue(Type.IsNumber(T.properties.a)) + Assert.IsTrue(Type.IsString(T.properties.b)) + Assert.IsEqual(T.required, ['a', 'b']) +}) +// ------------------------------------------------------------------ +// EvaluatePropertyUnion +// ------------------------------------------------------------------ +Test('Should EvaluatePropertyUnion 1', () => { + const A = Type.Readonly(Type.Number()) + const B = Type.String() + const T: Type.TReadonly> = Type.EvaluatePropertyUnion(A, B) + Assert.IsTrue(Type.IsUnion(T)) + Assert.IsTrue(Type.IsReadonly(T)) + Assert.IsFalse(Type.IsOptional(T)) +}) +Test('Should EvaluatePropertyUnion 2', () => { + const A = Type.Optional(Type.Number()) + const B = Type.String() + const T: Type.TOptional> = Type.EvaluatePropertyUnion(A, B) + Assert.IsTrue(Type.IsUnion(T)) + Assert.IsFalse(Type.IsReadonly(T)) + Assert.IsTrue(Type.IsOptional(T)) +}) +Test('Should EvaluatePropertyUnion 3', () => { + const A = Type.Number() + const B = Type.String() + const T: Type.TUnion<[Type.TNumber, Type.TString]> = Type.EvaluatePropertyUnion(A, B) + Assert.IsTrue(Type.IsUnion(T)) + Assert.IsFalse(Type.IsReadonly(T)) + Assert.IsFalse(Type.IsOptional(T)) +}) +Test('Should EvaluatePropertyUnion 4', () => { + const A = Type.Optional(Type.Number()) + const B = Type.Readonly(Type.String()) + const T: Type.TReadonly>> = Type.EvaluatePropertyUnion(A, B) + Assert.IsTrue(Type.IsUnion(T)) + Assert.IsTrue(Type.IsReadonly(T)) + Assert.IsTrue(Type.IsOptional(T)) + Assert.IsTrue(Type.IsNumber(T.anyOf[0])) + Assert.IsTrue(Type.IsString(T.anyOf[1])) + Assert.IsFalse(Type.IsReadonly(T.anyOf[0])) + Assert.IsFalse(Type.IsOptional(T.anyOf[0])) + Assert.IsFalse(Type.IsReadonly(T.anyOf[1])) + Assert.IsFalse(Type.IsOptional(T.anyOf[1])) +}) diff --git a/test/typebox/runtime/value/check/omit.ts b/test/typebox/runtime/value/check/omit.ts index 1542c1081..0dba83aeb 100644 --- a/test/typebox/runtime/value/check/omit.ts +++ b/test/typebox/runtime/value/check/omit.ts @@ -81,3 +81,122 @@ Test('Should support Omit of Never', () => { Fail(T, { y: 1, z: 1 }) Ok(T, { x: 1, y: 1, z: 1 }) }) +// ------------------------------------------------------------------ +// Retain Modifiers +// +// Ref: https://github.com/sinclairzx81/typebox/issues/1718 +// ------------------------------------------------------------------ +Test('Should retain optionality when omitting an intersection property', () => { + const T = Type.Omit( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('z') + ) + Ok(T, {}) + Ok(T, { x: { a: 1, b: 1 } }) + Fail(T, { x: { a: 1 } }) + Fail(T, { x: { b: 1 } }) +}) +Test('Should retain required properties when omitting an unknown intersection property', () => { + const T = Type.Omit( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('c') + ) + Ok(T, { z: 1 }) + Ok(T, { x: { a: 1, b: 1 }, z: 1 }) + Fail(T, {}) + Fail(T, { x: { a: 1 }, z: 1 }) +}) +Test('Should retain optionality when omitting never from an intersection', () => { + const T = Type.Omit( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Never() + ) + Ok(T, { z: 1 }) + Ok(T, { x: { a: 1, b: 1 }, z: 1 }) + Fail(T, { x: { a: 1 }, z: 1 }) +}) +Test('Should retain optionality when omitting a union property', () => { + const T = Type.Omit( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('z') + ) + Ok(T, {}) + Ok(T, { x: 1 }) + Ok(T, { x: 'value' }) + Fail(T, { x: true }) +}) +Test('Should retain required properties when omitting an unknown union property', () => { + const T = Type.Omit( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('c') + ) + Ok(T, { z: 1 }) + Ok(T, { x: 1, z: 1 }) + Ok(T, { x: 'value', z: 1 }) + Fail(T, {}) + Fail(T, { x: true, z: 1 }) +}) +Test('Should retain optionality when omitting never from a union', () => { + const T = Type.Omit( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Never() + ) + Ok(T, { z: 1 }) + Ok(T, { x: 1, z: 1 }) + Ok(T, { x: 'value', z: 1 }) + Fail(T, { x: true, z: 1 }) +}) diff --git a/test/typebox/runtime/value/check/pick.ts b/test/typebox/runtime/value/check/pick.ts index 84efe42b5..8153ffe5a 100644 --- a/test/typebox/runtime/value/check/pick.ts +++ b/test/typebox/runtime/value/check/pick.ts @@ -81,3 +81,118 @@ Test('Should support Pick of Never', () => { Fail(T, { x: 1, y: 1, z: 1 }) Ok(T, {}) }) +// ------------------------------------------------------------------ +// Retain Modifiers +// +// Ref: https://github.com/sinclairzx81/typebox/issues/1718 +// ------------------------------------------------------------------ +Test('Should retain optionality when picking an intersection property', () => { + const T = Type.Pick( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('x') + ) + Ok(T, {}) + Ok(T, { x: { a: 1, b: 1 } }) + Fail(T, { x: { a: 1 } }) + Fail(T, { x: { b: 1 } }) +}) +Test('Should pick no properties for an unknown intersection key', () => { + const T = Type.Pick( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('c'), + { additionalProperties: false } + ) + Ok(T, {}) + Fail(T, { x: { a: 1, b: 1 } }) +}) +Test('Should pick no properties from an intersection with never', () => { + const T = Type.Pick( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Never(), + { additionalProperties: false } + ) + Ok(T, {}) + Fail(T, { x: { a: 1, b: 1 } }) +}) +Test('Should retain optionality when picking a union property', () => { + const T = Type.Pick( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('x') + ) + Ok(T, {}) + Ok(T, { x: 1 }) + Ok(T, { x: 'value' }) + Fail(T, { x: true }) +}) +Test('Should pick no properties for an unknown union key', () => { + const T = Type.Pick( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('c'), + { additionalProperties: false } + ) + Ok(T, {}) + Fail(T, { x: 1 }) +}) +Test('Should pick no properties from a union with never', () => { + const T = Type.Pick( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Never(), + { additionalProperties: false } + ) + Ok(T, {}) + Fail(T, { x: 1 }) +}) diff --git a/test/typebox/static/type/omit.ts b/test/typebox/static/type/omit.ts new file mode 100644 index 000000000..6ecad7196 --- /dev/null +++ b/test/typebox/static/type/omit.ts @@ -0,0 +1,135 @@ +import { type Static, Type } from 'typebox' +import { Assert } from 'test' + +// ------------------------------------------------------------------ +// Retain Modifiers +// +// Ref: https://github.com/sinclairzx81/typebox/issues/1718 +// ------------------------------------------------------------------ +{ + const T = Type.Omit( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('z') + ) + type T = Static + Assert.IsExtendsMutual(true) +} +{ + const T = Type.Omit( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('c') + ) + type T = Static + Assert.IsExtendsMutual(true) +} +{ + const T = Type.Omit( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Never() + ) + type T = Static + Assert.IsExtendsMutual(true) +} +{ + const T = Type.Omit( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('z') + ) + type T = Static + Assert.IsExtendsMutual(true) +} +{ + const T = Type.Omit( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('c') + ) + type T = Static + Assert.IsExtendsMutual(true) +} +{ + const T = Type.Omit( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Never() + ) + type T = Static + Assert.IsExtendsMutual(true) +} diff --git a/test/typebox/static/type/pick.ts b/test/typebox/static/type/pick.ts new file mode 100644 index 000000000..3511e38be --- /dev/null +++ b/test/typebox/static/type/pick.ts @@ -0,0 +1,117 @@ +import { type Static, Type } from 'typebox' +import { Assert } from 'test' + +// ------------------------------------------------------------------ +// Retain Modifiers +// +// Ref: https://github.com/sinclairzx81/typebox/issues/1718 +// ------------------------------------------------------------------ +{ + const T = Type.Pick( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('x') + ) + type T = Static + Assert.IsExtendsMutual(true) +} +{ + const T = Type.Pick( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Literal('c') + ) + type T = Static + Assert.IsExtendsMutual(true) +} +{ + const T = Type.Pick( + Type.Intersect([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ a: Type.Number() }))), + z: Type.Number() + }), + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Object({ b: Type.Number() }))), + z: Type.Number() + }) + ]), + Type.Never() + ) + type T = Static + Assert.IsExtendsMutual(true) +} +{ + const T = Type.Pick( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('x') + ) + type T = Static + Assert.IsExtendsMutual(true) +} +{ + const T = Type.Pick( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Literal('c') + ) + type T = Static + Assert.IsExtendsMutual(true) +} +{ + const T = Type.Pick( + Type.Union([ + Type.Object({ + x: Type.Readonly(Type.Optional(Type.Number())), + z: Type.Number() + }), + Type.Object({ + x: Type.String(), + z: Type.Number() + }) + ]), + Type.Never() + ) + type T = Static + Assert.IsExtendsMutual(true) +}