Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/1.3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
---

### Version Updates
- [Revision 1.3.13](https://github.com/sinclairzx81/typebox/pull/1666)
- Composite Left Right Evaluate Order
- Dependent Type uses full Intersect and Union Evaluation
- Any, Never and Unknown support Order Independent Evaluation
- Single Pass Broaden Filter Optimization
- [Revision 1.3.12](https://github.com/sinclairzx81/typebox/pull/1665)
- Internal IDNA Submodule for IsHostname and IsIdnHostname
- General Specification Alignment
Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ type User = Type.Static<typeof User> // type User = {

[Documentation](https://sinclairzx81.github.io/typebox/#/docs/script/overview) | [Example 1](https://www.typescriptlang.org/play/?target=99&module=7#code/JYWwDg9gTgLgBAFQJ5gKZwGZQiOByGFVAIwgA88AoSgehrgFkBDGAC0YgBMBXAG1WoBjCADsAzvGZs4AXkREAdAGVBUYGBgAKAAaU4cQmjgA1VIJjQALLLgBvOGQBccEdxDFUUADRwkz1+6ePgBe-m4e3nAA7mGBUHAAvnoGRCZmFlAAzDb2Ti7hQb6xESHFnonJhuim5tAATDkOZZF++XEV2gCU1HRwAOJQTGCswIJiHDz8QqIS-YPDo+NyyGjKqupaUqw+uvpVabCoZDnJ+pBiwDDAos41GZZepy7QIEy8t+nQmY-6+twAbh9alA6skkntUn1UDhUDAoEgTr84P9PFdBKgxB9DmQANoAXR+v2AIk4owxzXxYMqqSknmAb0RvyY7mAqBEMCB90J+lJGAw3DEqE5Vm5cDEaEEfCYUGFUEsVIhRgYGPYclsTwA5tCQLD4c4oTC4UhRa8YHS3s5aWo3lSutR9sqxKr5KslDAWKMADxVCAYOZDEZjHF4R2sPB4gB8+l6DpVjKRCcTSeTKZTvX0WsNerscAUecSotTReLJfTcFN5veObzCgqJfrDeTvQSQA) | [Example 2](https://www.typescriptlang.org/play/?target=99&module=7#code/JYWwDg9gTgLgBAFQJ5gKZwGZQiOByGFVAIwgA88AoSgehrgFkBDGAC0YgBMBXAG1WoBjCADsAzvABqqQTGgAWOAF5ERAHQB5YgCsZMABQBvOGQBcqtGoBy3EMVRR9ASgA0cJOeSWbdh87cAXp7qPvaOrnAA7sHetmHOcAC+TpTC4lJ60ADMyhaomjp6RiYx+aF+ER551nEVgaU1vuFJKWkScNKy0ABMuV75WrqyxWbV5eFuVf2N8U4t1HRwAOJQTGCswIJiHDz8QqLt0rCoZH3qg0WGlHBwkGLAMMCi5p1yUPIu13Ai0CBMvC9MlAsp8btwAG6ArpQbqUZKpA7wJaoHCoGBQJBnSwXYZXG7ghyPQSoMQNACCUFWSH0RxgJ1cX2AIk4mxJ5MpTGp03GzhS8La8GYdKgwH+WIGhVxXyYdmAqBEMChbw+XxZGAw3DEqCVClBcDEaEEfCYUB170+-MRjBJ7BU0xxBjxcAA5iiQGiMeZkaj0Ug9X9haKAYwWA4g3CUpRCGhrWJbdUAMowFibAA80ZRGFjrAAfDd8wXC4sM9nck7CxXK1XqzX84sbq6fZ64MY1G2knra13u1363AA2H-uZW+3El8exPJ3X6IkgA)

TypeBox includes a runtime TypeScript engine that can transform TypeScript definitions to JSON Schema. The engine is fully type-safe and supports many programmable constructs including Conditional, Mapped, Indexed, Generics, Distributive Generics, and more.
TypeBox includes a micro TypeScript engine that can transform TypeScript definitions to JSON Schema. The engine is fully type-safe and supports many programmable constructs including Conditional, Mapped, Indexed, Generics, Distributive Generics, and more.

### Example

Expand Down
2 changes: 1 addition & 1 deletion src/guard/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function NextGraphemeClusterIndex(value: string, clusterStart: number): number {
// Consume combining marks & variation selectors
clusterEnd = ConsumeModifiers(value, clusterEnd)
// Handle multi-ZWJ sequences
while (clusterEnd < value.length - 1 && value[clusterEnd] === '\u200D') {
while (clusterEnd < value.length - 1 && IsZeroWidthJoiner(value.codePointAt(clusterEnd)!)) {
const nextCP = value.codePointAt(clusterEnd + 1)!
clusterEnd += 1 + CodePointLength(nextCP)
clusterEnd = ConsumeModifiers(value, clusterEnd)
Expand Down
2 changes: 1 addition & 1 deletion src/system/memory/clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function IsSchemaObject(value: Record<PropertyKey, unknown>): boolean {
}
function FromSchemaObject(value: Record<PropertyKey, unknown>): Record<PropertyKey, unknown> {
const result = {} as Record<PropertyKey, unknown>
for (const key of Object.getOwnPropertyNames(value)) {
for (const key of Guard.Keys(value)) {
if (Guard.IsUnsafePropertyKey(key)) continue // (ignore: prototype-pollution)
const descriptor = Object.getOwnPropertyDescriptor(value, key)! // safe-name!
descriptor.value = FromValue(descriptor.value)
Expand Down
3 changes: 2 additions & 1 deletion src/system/memory/discard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ THE SOFTWARE.
// deno-lint-ignore-file no-explicit-any
// deno-fmt-ignore-file

import { Guard } from '../../guard/index.ts'
import { Metrics } from './metrics.ts'
import { Clone } from './clone.ts'

Expand All @@ -38,7 +39,7 @@ type ObjectLike = Record<PropertyKey, any>
export function Discard(value: ObjectLike, propertyKeys: PropertyKey[]): ObjectLike {
Metrics.discard += 1
const result: ObjectLike = {}
for (const key of Object.getOwnPropertyNames(value)) {
for (const key of Guard.Keys(value)) {
if (propertyKeys.includes(key)) continue
const descriptor = Object.getOwnPropertyDescriptor(value, key)! // safe-name!
descriptor.value = Clone(descriptor.value)
Expand Down
138 changes: 63 additions & 75 deletions src/type/engine/evaluate/broaden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,106 +33,95 @@ import { type TSchema } from '../../types/schema.ts'
import { type TAny, IsAny } from '../../types/any.ts'
import { type TNever, IsNever } from '../../types/never.ts'
import { type TObject, IsObject } from '../../types/object.ts'
import { type TCompare, type TCompareResult, Compare, ResultRightInside, ResultLeftInside, ResultEqual } from './compare.ts'
import { type TUnknown, IsUnknown } from '../../types/unknown.ts'

import { type TCompare, Compare, CompareResultLeftInside, CompareResultEqual, CompareResultDisjoint } from './compare.ts'
import { type TFlatten, Flatten } from './flatten.ts'
import { type TEvaluateType, EvaluateType } from './evaluate.ts'

// ------------------------------------------------------------------
// BroadFilter
//
// BroadFilter iterates through a list of types and removes any
// that are less broad than a given reference type. It ensures that
// only the most generalized types remain.
// BroadenFilter
//
// ------------------------------------------------------------------
type TBroadFilter<Type extends TSchema, Types extends TSchema[], Result extends TSchema[] = []> = (
Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]]
? TCompare<Type, Left> extends typeof ResultRightInside
? TBroadFilter<Type, Right, [...Result]>
: TBroadFilter<Type, Right, [...Result, Left]>
: Result
)
function BroadFilter(type: TSchema, types: TSchema[]): TSchema[] {
return types.filter(left => {
return (Compare(type, left) as TCompareResult) === ResultRightInside
? false
: true
})
}
// ------------------------------------------------------------------
// GuardBroadestType
// Compares Type against each element of Types. If an existing element
// already subsumes Type, Type is discarded and the original list is
// returned untouched. Otherwise, any existing element that Type
// subsumes is dropped, and Type is appended to what remains.
//
// Tests if the given Type is broader than those in Types.
// ------------------------------------------------------------------
type TIsBroadestType<Type extends TSchema, Types extends TSchema[]> = (
type TBroadenFilter<Type extends TSchema, Types extends TSchema[], Result extends TSchema[] = [], All extends TSchema[] = Types> = (
Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]]
? TCompare<Type, Left> extends typeof ResultLeftInside | typeof ResultEqual
? false
: TIsBroadestType<Type, Right>
: true
? TCompare<Type, Left> extends typeof CompareResultLeftInside | typeof CompareResultEqual
? All // Left in set. Return original All set.
: TCompare<Type, Left> extends typeof CompareResultDisjoint
? TBroadenFilter<Type, Right, [...Result, Left], All> // Left is disjoint, keep it
: TBroadenFilter<Type, Right, Result, All> // Left in Type, drop it
: [...Result, Type] // Type broadest in set
)
function IsBroadestType(type: TSchema, types: TSchema[]): boolean {
const result = types.some(left => {
const result = Compare(type, left)
return Guard.IsEqual(result, ResultLeftInside) || Guard.IsEqual(result, ResultEqual)
})
return Guard.IsEqual(result, false)
function BroadenFilter<Type extends TSchema, Types extends TSchema[]>
(type: Type, types: [...Types], result: TSchema[] = [], all: TSchema[] = types): TBroadenFilter<Type, Types> {
return Guard.ShiftLeft(types, (left, right) => {
const compare = Compare(type, left)
return (
(Guard.IsEqual(compare, CompareResultLeftInside) || Guard.IsEqual(compare, CompareResultEqual))
? all // Left in set. Return original All set.
: Guard.IsEqual(compare, CompareResultDisjoint)
? BroadenFilter(type, right, [...result, left], all) // Left is disjoint, keep it
: BroadenFilter(type, right, result, all) // Left in Type, drop it
)
}, () => [...result, type]) as never // Type broadest in set
}
// ------------------------------------------------------------------
// BroadenType
//
// This function attempts to push the given Type into the broadest
// set if the type is either disjoint, or more broad than an existing
// element in the set.
// Evaluates a single Type and folds it into the accumulated Result,
// then continues on to the remaining Types. Any and Unknown terminate
// immediately: both are top types that dominate every other type
// unconditionally, so the remaining Types and Result are discarded
// outright rather than compared.
//
// TObject is pushed into Result without comparison, since comparing
// objects is currently too expensive.
//
// (revise-candidate-fast-path-property-sets)
//
// ------------------------------------------------------------------
type TBroadenType<Type extends TSchema, Types extends TSchema[],
Evaluated extends TSchema = TEvaluateType<Type>,
Result extends TSchema[] = (
Evaluated extends TAny ? [Evaluated] :
TIsBroadestType<Evaluated, Types> extends true
? [...TBroadFilter<Evaluated, Types>, Evaluated]
: Types
)
> = Result
function BroadenType<Type extends TSchema, Types extends TSchema[]>(type: Type, types: [...Types]): TBroadenType<Type, Types> {
type TBroadenType<Type extends TSchema, Types extends TSchema[], Result extends TSchema[],
Evaluated extends TSchema = TEvaluateType<Type>
> = (
Evaluated extends TAny ? [Evaluated] : // terminate (always the most broad)
Evaluated extends TUnknown ? [Evaluated] : // terminate (always the most broad)
Evaluated extends TNever ? TBroadenTypes<Types, Result> : // ignored: never is dropped
Evaluated extends TObject ? TBroadenTypes<Types, [...Result, Evaluated]> : // objects are always considered (too expensive to compare)
TBroadenTypes<Types, TBroadenFilter<Evaluated, Result>>
)
function BroadenType<Type extends TSchema, Types extends TSchema[], Result extends TSchema[]>
(type: Type, types: [...Types], result: [...Result]): TBroadenType<Type, Types, Result> {
const evaluated = EvaluateType(type)
return (
IsAny(evaluated) ? [evaluated] :
IsBroadestType(evaluated, types)
? [...BroadFilter(evaluated, types), evaluated]
: types
IsAny(evaluated) ? [evaluated] : // terminate (always the most broad)
IsUnknown(evaluated) ? [evaluated] : // terminate (always the most broad)
IsNever(evaluated) ? BroadenTypes(types, result) : // ignored: never is dropped
IsObject(evaluated) ? BroadenTypes(types, [...result, evaluated]) : // objects are always considered (too expensive to compare)
BroadenTypes(types, BroadenFilter(evaluated, result))
) as never
}
// ------------------------------------------------------------------
// BroadenTypes
//
// Note: We consider TObject types too expensive to apply a broaden
// operation to (currently) so we just push the TObject into the
// set on assumption that it is 'probably' distinct. TypeScript
// seems to do the same in some contexts. We should revise this
// code and revisit the TIsBroadestType, but expect we need
// TExtends optimizations first.
//
// (revise-candidate-fast-path-property-sets)
// Folds a list of Types into their broadest set. Each element is
// handed to BroadenType along with the remaining list and the
// accumulator, so BroadenType decides how iteration continues.
//
// ------------------------------------------------------------------
type TBroadenTypes<Types extends TSchema[], Result extends TSchema[] = []> = (
Types extends [infer Left extends TSchema, ...infer Right extends TSchema[]]
? (
Left extends TObject ? TBroadenTypes<Right, [...Result, Left]> : // push
Left extends TNever ? TBroadenTypes<Right, Result> : // ignore
TBroadenTypes<Right, TBroadenType<Left, Result>> // broaden
) : Result
? TBroadenType<Left, Right, Result>
: Result
)
function BroadenTypes<Types extends TSchema[]>(types: [...Types]): TBroadenTypes<Types> {
return types.reduce<TSchema[]>((result, left) => {
return (
IsObject(left) ? [...result, left] : // push
IsNever(left) ? result : // ignore
BroadenType(left, result) // broaden
)
}, []) as never
function BroadenTypes<Types extends TSchema[]>(types: [...Types], result: TSchema[] = []): TBroadenTypes<Types> {
return Guard.ShiftLeft(types, (left, right) => (
BroadenType(left, right, result)
), () => result) as never
}
// ------------------------------------------------------------------
// Broaden
Expand All @@ -146,5 +135,4 @@ export function Broaden<Types extends TSchema[]>(types: [...Types]): TBroaden<Ty
const broadened = BroadenTypes(types) as TSchema[]
const flattened = Flatten(broadened) as TSchema[]
return flattened as never
}

}
49 changes: 21 additions & 28 deletions src/type/engine/evaluate/compare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,45 +30,38 @@ THE SOFTWARE.
// deno-fmt-ignore-file

import { type TSchema } from '../../types/index.ts'
import { type TUnknown, IsUnknown } from '../../types/unknown.ts'
import { type TExtends, Extends, ExtendsResult } from "../../extends/index.ts"

// ------------------------------------------------------------------
// TCompare
// ------------------------------------------------------------------
export const ResultEqual = 'equal'
export const ResultDisjoint = 'disjoint'
export const ResultLeftInside = 'left-inside'
export const ResultRightInside = 'right-inside'

export type TCompareResult =
| typeof ResultEqual // left and right equal
| typeof ResultDisjoint // left and right are disjoint
| typeof ResultLeftInside // left inside right set
| typeof ResultRightInside // right inside left set
export const CompareResultEqual = 0 // 'equal'
export const CompareResultDisjoint = 1 // 'disjoint'
export const CompareResultLeftInside = 2 // 'left-inside'
export const CompareResultRightInside = 3 // 'right-inside'

export type TCompareResult = (
| typeof CompareResultEqual // left and right equal
| typeof CompareResultDisjoint // left and right are disjoint
| typeof CompareResultLeftInside // left inside right set
| typeof CompareResultRightInside // right inside left set
)
/** Compares left and right types and determines their set relationship */
export type TCompare<Left extends TSchema, Right extends TSchema,
Extends extends [ExtendsResult.TResult, ExtendsResult.TResult] = [
Left extends TUnknown ? ExtendsResult.TExtendsFalse : TExtends<{}, Left, Right>,
Left extends TUnknown ? ExtendsResult.TExtendsTrue : TExtends<{}, Right, Left>,
]
> = ( // TCompareResult
Extends extends [ExtendsResult.TExtendsTrueLike, ExtendsResult.TExtendsTrueLike] ? typeof ResultEqual :
Extends extends [ExtendsResult.TExtendsTrueLike, ExtendsResult.TExtendsFalse] ? typeof ResultLeftInside :
Extends extends [ExtendsResult.TExtendsFalse, ExtendsResult.TExtendsTrueLike] ? typeof ResultRightInside :
typeof ResultDisjoint
Extends extends [ExtendsResult.TResult, ExtendsResult.TResult] = [TExtends<{}, Left, Right>, TExtends<{}, Right, Left>]
> = (
Extends extends [ExtendsResult.TExtendsTrueLike, ExtendsResult.TExtendsTrueLike] ? typeof CompareResultEqual :
Extends extends [ExtendsResult.TExtendsTrueLike, ExtendsResult.TExtendsFalse] ? typeof CompareResultLeftInside :
Extends extends [ExtendsResult.TExtendsFalse, ExtendsResult.TExtendsTrueLike] ? typeof CompareResultRightInside :
typeof CompareResultDisjoint
)
/** Compares left and right types and determines their set relationship. */
export function Compare<Left extends TSchema, Right extends TSchema>(left: Left, right: Right): TCompare<Left, Right> {
const extendsCheck = [
IsUnknown(left) ? ExtendsResult.ExtendsFalse() : Extends({}, left, right),
IsUnknown(left) ? ExtendsResult.ExtendsTrue({}) : Extends({}, right, left),
]
const extendsCheck = [Extends({}, left, right), Extends({}, right, left)]
return (
ExtendsResult.IsExtendsTrueLike(extendsCheck[0]) && ExtendsResult.IsExtendsTrueLike(extendsCheck[1]) ? ResultEqual :
ExtendsResult.IsExtendsTrueLike(extendsCheck[0]) && ExtendsResult.IsExtendsFalse(extendsCheck[1]) ? ResultLeftInside :
ExtendsResult.IsExtendsFalse(extendsCheck[0]) && ExtendsResult.IsExtendsTrueLike(extendsCheck[1]) ? ResultRightInside :
ResultDisjoint
ExtendsResult.IsExtendsTrueLike(extendsCheck[0]) && ExtendsResult.IsExtendsTrueLike(extendsCheck[1]) ? CompareResultEqual :
ExtendsResult.IsExtendsTrueLike(extendsCheck[0]) && ExtendsResult.IsExtendsFalse(extendsCheck[1]) ? CompareResultLeftInside :
ExtendsResult.IsExtendsFalse(extendsCheck[0]) && ExtendsResult.IsExtendsTrueLike(extendsCheck[1]) ? CompareResultRightInside :
CompareResultDisjoint
) as never
}
Loading
Loading