mirror of
https://github.com/zoriya/typebox.git
synced 2025-12-06 06:46:10 +00:00
Exact Optional Property Types (#352)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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}))`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,33 +135,40 @@ export namespace ValueErrors {
|
||||
// ----------------------------------------------------------------------
|
||||
// Guards
|
||||
// ----------------------------------------------------------------------
|
||||
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 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 IsNumber(value: unknown): value is number {
|
||||
const result = typeof value === 'number'
|
||||
return TypeSystem.AllowNaN ? result : result && globalThis.Number.isFinite(value)
|
||||
}
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
@@ -47,33 +47,39 @@ export namespace ValueCheck {
|
||||
// ----------------------------------------------------------------------
|
||||
// Guards
|
||||
// ----------------------------------------------------------------------
|
||||
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 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 IsNumber(value: unknown): value is number {
|
||||
const result = typeof value === 'number'
|
||||
return TypeSystem.AllowNaN ? result : result && globalThis.Number.isFinite(value)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, {})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) })
|
||||
|
||||
Reference in New Issue
Block a user