Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sinclairzx81/typebox
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.26.2
Choose a base ref
...
head repository: sinclairzx81/typebox
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 0.26.3
Choose a head ref
  • 3 commits
  • 10 files changed
  • 1 contributor

Commits on Mar 23, 2023

  1. Composite Evaluate

    sinclairzx81 committed Mar 23, 2023
    Copy the full SHA
    e16fceb View commit details
  2. Composite Evaluate

    sinclairzx81 authored Mar 23, 2023
    Copy the full SHA
    4ae9125 View commit details

Commits on Mar 24, 2023

  1. Copy the full SHA
    39afc1f View commit details
Showing with 118 additions and 55 deletions.
  1. +2 −2 changelog/0.26.2.md
  2. +1 −1 package.json
  3. +7 −7 readme.md
  4. +17 −14 src/compiler/compiler.ts
  5. +20 −13 src/errors/errors.ts
  6. +11 −0 src/system/system.ts
  7. +19 −13 src/value/check.ts
  8. +3 −3 test/runtime/compiler/object.ts
  9. +36 −0 test/runtime/system/system.ts
  10. +2 −2 test/runtime/value/check/object.ts
4 changes: 2 additions & 2 deletions changelog/0.26.2.md
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ As such, 0.26.2 reverts back to the 0.25.0 interpretation, but applies type mapp
{ // evaluation case 4: all optional
type T = { a?: number } & { a?: number }

type C = {[K in keyof T]: T[K] } // type C = { a: number | undefined }
type C = {[K in keyof T]: T[K] } // type C = { a?: number | undefined }
}
```
Note: the Type.Composite is intended to be a temporary type which can be replaced with a more general `Type.Mapped` in future revisions of TypeBox. As the infrastructure to support mapped types does not exist, users can use Type.Composite to partially replicate mapped type evaluation for composited object types only.
Note: the Type.Composite is intended to be a temporary type which can be replaced with a more general `Type.Mapped` in future revisions of TypeBox. As the infrastructure to support mapped types does not exist, users can use Type.Composite to partially replicate mapped type evaluation for composited object types only.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sinclair/typebox",
"version": "0.26.2",
"version": "0.26.3",
"description": "JSONSchema Type Builder with Static Type Resolution for TypeScript",
"keywords": [
"typescript",
14 changes: 7 additions & 7 deletions readme.md
Original file line number Diff line number Diff line change
@@ -325,15 +325,15 @@ The following table lists the Standard TypeBox types. These types are fully comp
│ │ │ } │
│ │ │ │
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Composite([ │ type T = { │ const T = { │
const T = Type.Composite([ │ type I = { │ const T = { │
│ Type.Object({ │ x: numbertype: 'object', │
x: Type.Number() │ y: numberrequired: ['x', 'y'], │
│ }), │ } properties: { │
│ Type.Object({ │ x: { │
x: Type.Number() │ } & { required: ['x', 'y'], │
│ }), │ y: numberproperties: { │
│ Type.Object({ │ } │ x: { │
│ y: Type.Number() │ │ type: 'number'
│ }) │ │ }, │
│ ]) │ │ y: { │
│ │ │ type: 'number'
│ }) │ type T = { │ }, │
│ ]) │ [K in keyof I]: I[K]y: { │
│ │ }type: 'number'
│ │ │ } │
│ │ │ } │
│ │ │ } │
31 changes: 17 additions & 14 deletions src/compiler/compiler.ts
Original file line number Diff line number Diff line change
@@ -144,10 +144,10 @@ export namespace TypeCompiler {
return typeof value === 'string'
}
// -------------------------------------------------------------------
// Overrides
// Polices
// -------------------------------------------------------------------
function IsNumberCheck(value: string): string {
return !TypeSystem.AllowNaN ? `(typeof ${value} === 'number' && Number.isFinite(${value}))` : `typeof ${value} === 'number'`
function IsExactOptionalProperty(value: string, key: string, expression: string) {
return TypeSystem.ExactOptionalPropertyTypes ? `('${key}' in ${value} ? ${expression} : true)` : `(${value}.${key} !== undefined ? ${expression} : true)`
}
function IsObjectCheck(value: string): string {
return !TypeSystem.AllowArrayObjects ? `(typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value}))` : `(typeof ${value} === 'object' && ${value} !== null)`
@@ -157,6 +157,9 @@ export namespace TypeCompiler {
? `(typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value}) && !(${value} instanceof Date) && !(${value} instanceof Uint8Array))`
: `(typeof ${value} === 'object' && ${value} !== null && !(${value} instanceof Date) && !(${value} instanceof Uint8Array))`
}
function IsNumberCheck(value: string): string {
return !TypeSystem.AllowNaN ? `(typeof ${value} === 'number' && Number.isFinite(${value}))` : `typeof ${value} === 'number'`
}
function IsVoidCheck(value: string): string {
return TypeSystem.AllowVoidNull ? `(${value} === undefined || ${value} === null)` : `${value} === undefined`
}
@@ -255,29 +258,29 @@ export namespace TypeCompiler {
yield IsObjectCheck(value)
if (IsNumber(schema.minProperties)) yield `Object.getOwnPropertyNames(${value}).length >= ${schema.minProperties}`
if (IsNumber(schema.maxProperties)) yield `Object.getOwnPropertyNames(${value}).length <= ${schema.maxProperties}`
const schemaKeys = globalThis.Object.getOwnPropertyNames(schema.properties)
for (const schemaKey of schemaKeys) {
const memberExpression = MemberExpression.Encode(value, schemaKey)
const property = schema.properties[schemaKey]
if (schema.required && schema.required.includes(schemaKey)) {
const knownKeys = globalThis.Object.getOwnPropertyNames(schema.properties)
for (const knownKey of knownKeys) {
const memberExpression = MemberExpression.Encode(value, knownKey)
const property = schema.properties[knownKey]
if (schema.required && schema.required.includes(knownKey)) {
yield* Visit(property, references, memberExpression)
if (Types.ExtendsUndefined.Check(property)) yield `('${schemaKey}' in ${value})`
if (Types.ExtendsUndefined.Check(property)) yield `('${knownKey}' in ${value})`
} else {
const expression = CreateExpression(property, references, memberExpression)
yield `('${schemaKey}' in ${value} ? ${expression} : true)`
yield IsExactOptionalProperty(value, knownKey, expression)
}
}
if (schema.additionalProperties === false) {
if (schema.required && schema.required.length === schemaKeys.length) {
yield `Object.getOwnPropertyNames(${value}).length === ${schemaKeys.length}`
if (schema.required && schema.required.length === knownKeys.length) {
yield `Object.getOwnPropertyNames(${value}).length === ${knownKeys.length}`
} else {
const keys = `[${schemaKeys.map((key) => `'${key}'`).join(', ')}]`
const keys = `[${knownKeys.map((key) => `'${key}'`).join(', ')}]`
yield `Object.getOwnPropertyNames(${value}).every(key => ${keys}.includes(key))`
}
}
if (typeof schema.additionalProperties === 'object') {
const expression = CreateExpression(schema.additionalProperties, references, 'value[key]')
const keys = `[${schemaKeys.map((key) => `'${key}'`).join(', ')}]`
const keys = `[${knownKeys.map((key) => `'${key}'`).join(', ')}]`
yield `(Object.getOwnPropertyNames(${value}).every(key => ${keys}.includes(key) || ${expression}))`
}
}
33 changes: 20 additions & 13 deletions src/errors/errors.ts
Original file line number Diff line number Diff line change
@@ -135,33 +135,40 @@ export namespace ValueErrors {
// ----------------------------------------------------------------------
// Guards
// ----------------------------------------------------------------------
function IsBigInt(value: unknown): value is bigint {
return typeof value === 'bigint'
}
function IsInteger(value: unknown): value is number {
return globalThis.Number.isInteger(value)
}
function IsString(value: unknown): value is string {
return typeof value === 'string'
}
function IsDefined<T>(value: unknown): value is T {
return value !== undefined
}
// ----------------------------------------------------------------------
// Policies
// ----------------------------------------------------------------------
function IsExactOptionalProperty(value: Record<keyof any, unknown>, key: string) {
return TypeSystem.ExactOptionalPropertyTypes ? key in value : value[key] !== undefined
}
function IsObject(value: unknown): value is Record<keyof any, unknown> {
const result = typeof value === 'object' && value !== null
return TypeSystem.AllowArrayObjects ? result : result && !globalThis.Array.isArray(value)
}
function IsRecordObject(value: unknown): value is Record<keyof any, unknown> {
return IsObject(value) && !(value instanceof globalThis.Date) && !(value instanceof globalThis.Uint8Array)
}
function IsBigInt(value: unknown): value is bigint {
return typeof value === 'bigint'
}
function IsNumber(value: unknown): value is number {
const result = typeof value === 'number'
return TypeSystem.AllowNaN ? result : result && globalThis.Number.isFinite(value)
}
function IsInteger(value: unknown): value is number {
return globalThis.Number.isInteger(value)
}
function IsString(value: unknown): value is string {
return typeof value === 'string'
}
function IsVoid(value: unknown): value is void {
const result = value === undefined
return TypeSystem.AllowVoidNull ? result || value === null : result
}
function IsDefined<T>(value: unknown): value is T {
return value !== undefined
}

// ----------------------------------------------------------------------
// Types
// ----------------------------------------------------------------------
@@ -351,7 +358,7 @@ export namespace ValueErrors {
yield { type: ValueErrorType.ObjectRequiredProperties, schema: property, path: `${path}/${knownKey}`, value: undefined, message: `Expected required property` }
}
} else {
if (knownKey in value) {
if (IsExactOptionalProperty(value, knownKey)) {
yield* Visit(property, references, `${path}/${knownKey}`, value[knownKey])
}
}
11 changes: 11 additions & 0 deletions src/system/system.ts
Original file line number Diff line number Diff line change
@@ -38,14 +38,24 @@ export class TypeSystemDuplicateFormat extends Error {
super(`Duplicate string format '${kind}' detected`)
}
}

/** Creates user defined types and formats and provides overrides for value checking behaviours */
export namespace TypeSystem {
// ------------------------------------------------------------------------
// Assertion Policies
// ------------------------------------------------------------------------
/** Sets whether TypeBox should assert optional properties using the TypeScript `exactOptionalPropertyTypes` assertion policy. The default is `false` */
export let ExactOptionalPropertyTypes: boolean = false
/** Sets whether arrays should be treated as a kind of objects. The default is `false` */
export let AllowArrayObjects: boolean = false
/** Sets whether `NaN` or `Infinity` should be treated as valid numeric values. The default is `false` */
export let AllowNaN: boolean = false
/** Sets whether `null` should validate for void types. The default is `false` */
export let AllowVoidNull: boolean = false

// ------------------------------------------------------------------------
// String Formats and Types
// ------------------------------------------------------------------------
/** Creates a new type */
export function Type<Type, Options = object>(kind: string, check: (options: Options, value: unknown) => boolean) {
if (Types.TypeRegistry.Has(kind)) throw new TypeSystemDuplicateTypeKind(kind)
@@ -58,6 +68,7 @@ export namespace TypeSystem {
Types.FormatRegistry.Set(format, check)
return format
}

// ------------------------------------------------------------------------
// Deprecated
// ------------------------------------------------------------------------
32 changes: 19 additions & 13 deletions src/value/check.ts
Original file line number Diff line number Diff line change
@@ -47,33 +47,39 @@ export namespace ValueCheck {
// ----------------------------------------------------------------------
// Guards
// ----------------------------------------------------------------------
function IsBigInt(value: unknown): value is bigint {
return typeof value === 'bigint'
}
function IsInteger(value: unknown): value is number {
return globalThis.Number.isInteger(value)
}
function IsString(value: unknown): value is string {
return typeof value === 'string'
}
function IsDefined<T>(value: unknown): value is T {
return value !== undefined
}
// ----------------------------------------------------------------------
// Policies
// ----------------------------------------------------------------------
function IsExactOptionalProperty(value: Record<keyof any, unknown>, key: string) {
return TypeSystem.ExactOptionalPropertyTypes ? key in value : value[key] !== undefined
}
function IsObject(value: unknown): value is Record<keyof any, unknown> {
const result = typeof value === 'object' && value !== null
return TypeSystem.AllowArrayObjects ? result : result && !globalThis.Array.isArray(value)
}
function IsRecordObject(value: unknown): value is Record<keyof any, unknown> {
return IsObject(value) && !(value instanceof globalThis.Date) && !(value instanceof globalThis.Uint8Array)
}
function IsBigInt(value: unknown): value is bigint {
return typeof value === 'bigint'
}
function IsNumber(value: unknown): value is number {
const result = typeof value === 'number'
return TypeSystem.AllowNaN ? result : result && globalThis.Number.isFinite(value)
}
function IsInteger(value: unknown): value is number {
return globalThis.Number.isInteger(value)
}
function IsString(value: unknown): value is string {
return typeof value === 'string'
}
function IsVoid(value: unknown): value is void {
const result = value === undefined
return TypeSystem.AllowVoidNull ? result || value === null : result
}
function IsDefined<T>(value: unknown): value is T {
return value !== undefined
}
// ----------------------------------------------------------------------
// Types
// ----------------------------------------------------------------------
@@ -237,7 +243,7 @@ export namespace ValueCheck {
return knownKey in value
}
} else {
if (knownKey in value && !Visit(property, references, value[knownKey])) {
if (IsExactOptionalProperty(value, knownKey) && !Visit(property, references, value[knownKey])) {
return false
}
}
6 changes: 3 additions & 3 deletions test/runtime/compiler/object.ts
Original file line number Diff line number Diff line change
@@ -227,16 +227,16 @@ describe('type/compiler/Object', () => {
Ok(T, { x: undefined })
Ok(T, {})
})
it('Should not check undefined for optional property of number', () => {
it('Should check undefined for optional property of number', () => {
const T = Type.Object({ x: Type.Optional(Type.Number()) })
Ok(T, { x: 1 })
Ok(T, { x: undefined }) // allowed by default
Ok(T, {})
Fail(T, { x: undefined })
})
it('Should check undefined for optional property of undefined', () => {
const T = Type.Object({ x: Type.Optional(Type.Undefined()) })
Fail(T, { x: 1 })
Ok(T, {})
Ok(T, { x: undefined })
Ok(T, {})
})
})
36 changes: 36 additions & 0 deletions test/runtime/system/system.ts
Original file line number Diff line number Diff line change
@@ -3,6 +3,42 @@ import { Assert } from '../assert/index'
import { TypeSystem } from '@sinclair/typebox/system'
import { Type } from '@sinclair/typebox'

describe('system/TypeSystem/ExactOptionalPropertyTypes', () => {
before(() => {
TypeSystem.ExactOptionalPropertyTypes = true
})
after(() => {
TypeSystem.ExactOptionalPropertyTypes = false
})
// ---------------------------------------------------------------
// Number
// ---------------------------------------------------------------
it('Should not validate optional number', () => {
const T = Type.Object({
x: Type.Optional(Type.Number()),
})
Ok(T, {})
Ok(T, { x: 1 })
Fail(T, { x: undefined })
})
it('Should not validate undefined', () => {
const T = Type.Object({
x: Type.Optional(Type.Undefined()),
})
Ok(T, {})
Fail(T, { x: 1 })
Ok(T, { x: undefined })
})
it('Should validate optional number | undefined', () => {
const T = Type.Object({
x: Type.Optional(Type.Union([Type.Number(), Type.Undefined()])),
})
Ok(T, {})
Ok(T, { x: 1 })
Ok(T, { x: undefined })
})
})

describe('system/TypeSystem/AllowNaN', () => {
before(() => {
TypeSystem.AllowNaN = true
4 changes: 2 additions & 2 deletions test/runtime/value/check/object.ts
Original file line number Diff line number Diff line change
@@ -212,11 +212,11 @@ describe('value/check/Object', () => {
Assert.equal(Value.Check(T, { x: undefined }), true)
Assert.equal(Value.Check(T, {}), true)
})
it('Should not check undefined for optional property of number', () => {
it('Should check undefined for optional property of number', () => {
const T = Type.Object({ x: Type.Optional(Type.Number()) })
Assert.equal(Value.Check(T, { x: 1 }), true)
Assert.equal(Value.Check(T, { x: undefined }), true) // allowed by default
Assert.equal(Value.Check(T, {}), true)
Assert.equal(Value.Check(T, { x: undefined }), false)
})
it('Should check undefined for optional property of undefined', () => {
const T = Type.Object({ x: Type.Optional(Type.Undefined()) })