diff --git a/hammer.mjs b/hammer.mjs index ef5d2b8..20af660 100644 --- a/hammer.mjs +++ b/hammer.mjs @@ -32,7 +32,7 @@ export async function benchmark() { // Test // ------------------------------------------------------------------------------- export async function test_typescript() { - for (const version of ['4.9.5', '5.0.4', '5.1.3', '5.1.6', '5.2.2', '5.3.2', '5.3.3', '5.4.3', '5.4.5', 'next', 'latest']) { + for (const version of ['4.9.5', '5.0.4', '5.1.3', '5.1.6', '5.2.2', '5.3.2', '5.3.3', '5.4.3', '5.4.5', '5.5.2', 'next', 'latest']) { await shell(`npm install typescript@${version} --no-save`) await test_static() } diff --git a/package-lock.json b/package-lock.json index 443474f..f75983e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.32.34", + "version": "0.32.35", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.32.34", + "version": "0.32.35", "license": "MIT", "devDependencies": { "@arethetypeswrong/cli": "^0.13.2", @@ -17,7 +17,7 @@ "ajv-formats": "^2.1.1", "mocha": "^10.4.0", "prettier": "^2.7.1", - "typescript": "^5.5.2" + "typescript": "^5.5.3" } }, "node_modules/@andrewbranch/untar.js": { @@ -1576,9 +1576,9 @@ } }, "node_modules/typescript": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", - "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2738,9 +2738,9 @@ "dev": true }, "typescript": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", - "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true }, "undici-types": { diff --git a/package.json b/package.json index 0afff5a..967cc46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.32.34", + "version": "0.32.35", "description": "Json Schema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", @@ -37,6 +37,6 @@ "ajv-formats": "^2.1.1", "mocha": "^10.4.0", "prettier": "^2.7.1", - "typescript": "^5.5.2" + "typescript": "^5.5.3" } } diff --git a/src/errors/function.ts b/src/errors/function.ts index 8bb1605..94504c5 100644 --- a/src/errors/function.ts +++ b/src/errors/function.ts @@ -124,7 +124,7 @@ export function DefaultErrorFunction(error: ErrorFunctionParameter) { case ValueErrorType.ObjectMinProperties: return `Expected object to have at least ${error.schema.minProperties} properties` case ValueErrorType.ObjectRequiredProperty: - return 'Required property' + return 'Expected required property' case ValueErrorType.Promise: return 'Expected Promise' case ValueErrorType.RegExp: diff --git a/src/type/patterns/patterns.ts b/src/type/patterns/patterns.ts index 016d604..9c9e910 100644 --- a/src/type/patterns/patterns.ts +++ b/src/type/patterns/patterns.ts @@ -29,6 +29,8 @@ THE SOFTWARE. export const PatternBoolean = '(true|false)' export const PatternNumber = '(0|[1-9][0-9]*)' export const PatternString = '(.*)' +export const PatternNever = '(?!.*)' export const PatternBooleanExact = `^${PatternBoolean}$` export const PatternNumberExact = `^${PatternNumber}$` export const PatternStringExact = `^${PatternString}$` +export const PatternNeverExact = `^${PatternNever}$` diff --git a/src/type/record/record.ts b/src/type/record/record.ts index e949027..cc6e9e7 100644 --- a/src/type/record/record.ts +++ b/src/type/record/record.ts @@ -29,6 +29,7 @@ THE SOFTWARE. import type { TSchema } from '../schema/index' import type { Static } from '../static/index' import type { Evaluate, Ensure, Assert } from '../helpers/index' +import { type TAny } from '../any/index' import { Object, type TObject, type TProperties, type TAdditionalProperties, type ObjectOptions } from '../object/index' import { type TLiteral, type TLiteralValue } from '../literal/index' import { Never, type TNever } from '../never/index' @@ -40,7 +41,7 @@ import { type TNumber } from '../number/index' import { type TEnum } from '../enum/index' import { IsTemplateLiteralFinite, TIsTemplateLiteralFinite, type TTemplateLiteral } from '../template-literal/index' -import { PatternStringExact, PatternNumberExact } from '../patterns/index' +import { PatternStringExact, PatternNumberExact, PatternNeverExact } from '../patterns/index' import { IndexPropertyKeys } from '../indexed/index' import { Kind, Hint } from '../symbols/index' import { CloneType } from '../clone/type' @@ -51,7 +52,7 @@ import { IsUndefined } from '../guard/value' // ------------------------------------------------------------------ // TypeGuard // ------------------------------------------------------------------ -import { IsInteger, IsLiteral, IsNumber, IsString, IsRegExp, IsTemplateLiteral, IsUnion } from '../guard/kind' +import { IsInteger, IsLiteral, IsAny, IsNever, IsNumber, IsString, IsRegExp, IsTemplateLiteral, IsUnion } from '../guard/kind' // ------------------------------------------------------------------ // RecordCreateFromPattern // ------------------------------------------------------------------ @@ -156,6 +157,28 @@ function FromStringKey(K: K, T: T, options return RecordCreateFromPattern(pattern, T, options) as never } // ------------------------------------------------------------------ +// FromAnyKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromAnyKey<_ extends TAny, T extends TSchema> = ( + Ensure> +) +// prettier-ignore +function FromAnyKey(K: K, T: T, options: ObjectOptions): TFromAnyKey { + return RecordCreateFromPattern(PatternStringExact, T, options) as never +} +// ------------------------------------------------------------------ +// FromNeverKey +// ------------------------------------------------------------------ +// prettier-ignore +type TFromNeverKey<_ extends TNever, T extends TSchema> = ( + Ensure> +) +// prettier-ignore +function FromNeverKey(K: K, T: T, options: ObjectOptions): TFromNeverKey { + return RecordCreateFromPattern(PatternNeverExact, T, options) as never +} +// ------------------------------------------------------------------ // FromIntegerKey // ------------------------------------------------------------------ // prettier-ignore @@ -205,6 +228,8 @@ export type TRecordOrObject = K extends TNumber ? TFromNumberKey : K extends TRegExp ? TFromRegExpKey : K extends TString ? TFromStringKey : + K extends TAny ? TFromAnyKey : + K extends TNever ? TFromNeverKey : TNever // ------------------------------------------------------------------ // TRecordOrObject @@ -220,6 +245,8 @@ export function Record(K: K, T: T, options IsNumber(K) ? FromNumberKey(K, T, options) : IsRegExp(K) ? FromRegExpKey(K, T, options) : IsString(K) ? FromStringKey(K, T, options) : + IsAny(K) ? FromAnyKey(K, T, options) : + IsNever(K) ? FromNeverKey(K, T, options) : Never(options) ) as never } diff --git a/src/value/convert/convert.ts b/src/value/convert/convert.ts index 9d81913..21d1dbb 100644 --- a/src/value/convert/convert.ts +++ b/src/value/convert/convert.ts @@ -203,6 +203,8 @@ function FromObject(schema: TObject, references: TSchema[], value: any): unknown return result } function FromRecord(schema: TRecord, references: TSchema[], value: any): unknown { + const isConvertable = IsObject(value) + if (!isConvertable) return value const propertyKey = Object.getOwnPropertyNames(schema.patternProperties)[0] const property = schema.patternProperties[propertyKey] const result = {} as Record diff --git a/test/runtime/compiler-ajv/record.ts b/test/runtime/compiler-ajv/record.ts index 75b739b..0dcba8f 100644 --- a/test/runtime/compiler-ajv/record.ts +++ b/test/runtime/compiler-ajv/record.ts @@ -238,4 +238,56 @@ describe('compiler-ajv/Record', () => { key2: 1, }) }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/916 + // ---------------------------------------------------------------- + it('Should validate for string keys', () => { + const T = Type.Record(Type.String(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) + it('Should validate for number keys', () => { + const T = Type.Record(Type.Number(), Type.Null(), { + additionalProperties: false, + }) + Fail(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + Ok(T, { + 0: null, + 1: null, + }) + }) + it('Should validate for any keys', () => { + const T = Type.Record(Type.Any(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) + it('Should validate for never keys', () => { + const T = Type.Record(Type.Never(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, {}) + Fail(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) }) diff --git a/test/runtime/compiler/record.ts b/test/runtime/compiler/record.ts index 8b7cfbb..af4417b 100644 --- a/test/runtime/compiler/record.ts +++ b/test/runtime/compiler/record.ts @@ -269,4 +269,56 @@ describe('compiler/Record', () => { key2: 1, }) }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/916 + // ---------------------------------------------------------------- + it('Should validate for string keys', () => { + const T = Type.Record(Type.String(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) + it('Should validate for number keys', () => { + const T = Type.Record(Type.Number(), Type.Null(), { + additionalProperties: false, + }) + Fail(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + Ok(T, { + 0: null, + 1: null, + }) + }) + it('Should validate for any keys', () => { + const T = Type.Record(Type.Any(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) + it('Should validate for never keys', () => { + const T = Type.Record(Type.Never(), Type.Null(), { + additionalProperties: false, + }) + Ok(T, {}) + Fail(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + }) }) diff --git a/test/runtime/type/guard/kind/record.ts b/test/runtime/type/guard/kind/record.ts index 12365ee..85ba4e9 100644 --- a/test/runtime/type/guard/kind/record.ts +++ b/test/runtime/type/guard/kind/record.ts @@ -1,4 +1,4 @@ -import { TypeGuard, PatternNumberExact, PatternStringExact, PatternString, PatternNumber } from '@sinclair/typebox' +import { TypeGuard, PatternNumberExact, PatternStringExact, PatternString, PatternNeverExact } from '@sinclair/typebox' import { Type } from '@sinclair/typebox' import { Assert } from '../../../assert/index' @@ -21,8 +21,11 @@ describe('guard/kind/TRecord', () => { }) it('Should guard overload 3', () => { // @ts-ignore - const T = Type.Record(Type.Union([]), Type.String(), { extra: 1 }) - Assert.IsTrue(TypeGuard.IsNever(T)) + const N = Type.Union([]) // Never + const T = Type.Record(N, Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNeverExact])) + Assert.IsEqual(T.extra, 1) }) it('Should guard overload 4', () => { // @ts-ignore @@ -89,6 +92,23 @@ describe('guard/kind/TRecord', () => { Assert.IsTrue(TypeGuard.IsNull(R.properties.Y)) Assert.IsTrue(TypeGuard.IsNull(R.properties.Z)) }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/916 + // ---------------------------------------------------------------- + it('Should guard overload 13', () => { + // @ts-ignore + const T = Type.Record(Type.Never(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNeverExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 14', () => { + // @ts-ignore + const T = Type.Record(Type.Any(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternStringExact])) + Assert.IsEqual(T.extra, 1) + }) // ------------------------------------------------------------- // Variants // ------------------------------------------------------------- diff --git a/test/runtime/type/guard/type/record.ts b/test/runtime/type/guard/type/record.ts index 1472996..38c7d1b 100644 --- a/test/runtime/type/guard/type/record.ts +++ b/test/runtime/type/guard/type/record.ts @@ -1,4 +1,4 @@ -import { TypeGuard, PatternNumberExact, PatternStringExact, PatternString, PatternNumber } from '@sinclair/typebox' +import { TypeGuard, PatternNumberExact, PatternStringExact, PatternNeverExact, PatternString, PatternNumber } from '@sinclair/typebox' import { Type } from '@sinclair/typebox' import { Assert } from '../../../assert/index' @@ -21,8 +21,11 @@ describe('guard/type/TRecord', () => { }) it('Should guard overload 3', () => { // @ts-ignore - const T = Type.Record(Type.Union([]), Type.String(), { extra: 1 }) - Assert.IsTrue(TypeGuard.IsNever(T)) + const N = Type.Union([]) // Never + const T = Type.Record(N, Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNeverExact])) + Assert.IsEqual(T.extra, 1) }) it('Should guard overload 4', () => { // @ts-ignore @@ -89,6 +92,25 @@ describe('guard/type/TRecord', () => { Assert.IsTrue(TypeGuard.IsNull(R.properties.Y)) Assert.IsTrue(TypeGuard.IsNull(R.properties.Z)) }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/916 + // + // Added overload for Any and Never Keys + // ---------------------------------------------------------------- + it('Should guard overload 13', () => { + // @ts-ignore + const T = Type.Record(Type.Never(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternNeverExact])) + Assert.IsEqual(T.extra, 1) + }) + it('Should guard overload 14', () => { + // @ts-ignore + const T = Type.Record(Type.Any(), Type.String(), { extra: 1 }) + Assert.IsTrue(TypeGuard.IsRecord(T)) + Assert.IsTrue(TypeGuard.IsString(T.patternProperties[PatternStringExact])) + Assert.IsEqual(T.extra, 1) + }) // ------------------------------------------------------------- // Variants // ------------------------------------------------------------- diff --git a/test/runtime/value/check/record.ts b/test/runtime/value/check/record.ts index 2e4bd73..c4b9c1f 100644 --- a/test/runtime/value/check/record.ts +++ b/test/runtime/value/check/record.ts @@ -236,4 +236,62 @@ describe('value/check/Record', () => { const R = Value.Check(T, { 1: '', 2: '', x: true }) Assert.IsEqual(R, true) }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/916 + // ---------------------------------------------------------------- + it('Should validate for string keys', () => { + const T = Type.Record(Type.String(), Type.Null(), { + additionalProperties: false, + }) + const R = Value.Check(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + Assert.IsEqual(R, true) + }) + it('Should validate for number keys', () => { + const T = Type.Record(Type.Number(), Type.Null(), { + additionalProperties: false, + }) + const R1 = Value.Check(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + const R2 = Value.Check(T, { + 0: null, + 1: null, + }) + Assert.IsEqual(R1, false) + Assert.IsEqual(R2, true) + }) + it('Should validate for any keys', () => { + const T = Type.Record(Type.Any(), Type.Null(), { + additionalProperties: false, + }) + const R = Value.Check(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + Assert.IsEqual(R, true) + }) + it('Should validate for never keys', () => { + const T = Type.Record(Type.Never(), Type.Null(), { + additionalProperties: false, + }) + const R1 = Value.Check(T, {}) + const R2 = Value.Check(T, { + a: null, + b: null, + 0: null, + 1: null, + }) + Assert.IsEqual(R1, true) + Assert.IsEqual(R2, false) + }) }) diff --git a/test/runtime/value/convert/record.ts b/test/runtime/value/convert/record.ts index 90c24f4..edd3b5a 100644 --- a/test/runtime/value/convert/record.ts +++ b/test/runtime/value/convert/record.ts @@ -8,4 +8,37 @@ describe('value/convert/Record', () => { const V = Value.Convert(T, { x: '42', y: '24', z: 'hello' }) Assert.IsEqual(V, { x: 42, y: 24, z: 'hello' }) }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/930 + // ---------------------------------------------------------------- + it('Should convert record union 1', () => { + const T = Type.Union([Type.Null(), Type.Record(Type.Number(), Type.Any())]) + const V = Value.Convert(T, {}) + Assert.IsEqual(V, {}) + }) + it('Should convert record union 2', () => { + const T = Type.Union([Type.Record(Type.Number(), Type.Any()), Type.Null()]) + const V = Value.Convert(T, {}) + Assert.IsEqual(V, {}) + }) + it('Should convert record union 3', () => { + const T = Type.Union([Type.Null(), Type.Record(Type.Number(), Type.Any())]) + const V = Value.Convert(T, null) + Assert.IsEqual(V, null) + }) + it('Should convert record union 4', () => { + const T = Type.Union([Type.Record(Type.Number(), Type.Any()), Type.Null()]) + const V = Value.Convert(T, null) + Assert.IsEqual(V, null) + }) + it('Should convert record union 5', () => { + const T = Type.Union([Type.Null(), Type.Record(Type.Number(), Type.Any())]) + const V = Value.Convert(T, 'NULL') + Assert.IsEqual(V, null) + }) + it('Should convert record union 6', () => { + const T = Type.Union([Type.Record(Type.Number(), Type.Any()), Type.Null()]) + const V = Value.Convert(T, 'NULL') + Assert.IsEqual(V, null) + }) }) diff --git a/test/static/record.ts b/test/static/record.ts index da8a028..ab18b06 100644 --- a/test/static/record.ts +++ b/test/static/record.ts @@ -189,3 +189,16 @@ import { Type, Static } from '@sinclair/typebox' '$propC': string }>() } +// ------------------------------------------------------------------ +// https://github.com/sinclairzx81/typebox/issues/916 +// ------------------------------------------------------------------ +{ + const K = Type.Any() + const T = Type.Record(K, Type.String()) + Expect(T).ToStatic>() +} +{ + const K = Type.Never() + const T = Type.Record(K, Type.String()) + Expect(T).ToStatic<{}>() +}