From 5f641c13aefe4697f282d6ee8f2727738629416b Mon Sep 17 00:00:00 2001 From: sinclairzx81 Date: Fri, 14 Apr 2023 13:11:35 +0900 Subject: [PATCH] Recursive KeyOf (#382) --- package-lock.json | 4 +- package.json | 2 +- readme.md | 2 +- src/typebox.ts | 15 ++++--- src/value/create.ts | 10 ++++- test/runtime/type/guard/index.ts | 1 + test/runtime/type/guard/keyof.ts | 59 ++++++++++++++++++++++++++ test/runtime/value/create/intersect.ts | 22 ++++++++++ test/static/keyof.ts | 12 ++++++ 9 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 test/runtime/type/guard/keyof.ts diff --git a/package-lock.json b/package-lock.json index 7c2f3e5..83f85c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.27.5", + "version": "0.27.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.27.5", + "version": "0.27.6", "license": "MIT", "devDependencies": { "@sinclair/hammer": "^0.17.1", diff --git a/package.json b/package.json index ca02865..04a9299 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.27.5", + "version": "0.27.6", "description": "JSONSchema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", diff --git a/readme.md b/readme.md index 6c6e443..c5120a9 100644 --- a/readme.md +++ b/readme.md @@ -402,7 +402,7 @@ The following table lists the Standard TypeBox types. These types are fully comp ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Record( │ type T = Record< │ const T = { │ │ Type.String(), │ string, │ type: 'object', │ -│ Type.Number() │ number, │ patternProperties: { │ +│ Type.Number() │ number │ patternProperties: { │ │ ) │ > │ '^.*$': { │ │ │ │ type: 'number' │ │ │ │ } │ diff --git a/src/typebox.ts b/src/typebox.ts index 5643995..21ad709 100644 --- a/src/typebox.ts +++ b/src/typebox.ts @@ -305,11 +305,12 @@ export type TKeyOfTuple = { : never // prettier-ignore export type TKeyOf = ( - T extends TComposite ? TKeyOfTuple : - T extends TIntersect ? TKeyOfTuple : - T extends TUnion ? TKeyOfTuple : - T extends TObject ? TKeyOfTuple : - T extends TRecord ? [K] : + T extends TComposite ? TKeyOfTuple : + T extends TIntersect ? TKeyOfTuple : + T extends TUnion ? TKeyOfTuple : + T extends TObject ? TKeyOfTuple : + T extends TRecursive ? TKeyOfTuple : + T extends TRecord ? [K] : [] ) extends infer R ? TUnionResult> : never // -------------------------------------------------------------------------- @@ -394,7 +395,7 @@ export interface TObject extends TSchema, O export type TOmitArray = Assert<{ [K2 in keyof T]: TOmit, K> }, TSchema[]> export type TOmitProperties = Evaluate, TProperties>> // prettier-ignore -export type TOmit = +export type TOmit = T extends TComposite ? TComposite> : T extends TIntersect ? TIntersect> : T extends TUnion ? TUnion> : @@ -436,7 +437,7 @@ export type TPickProperties = [K in keyof R]: Assert extends TSchema ? R[K] : never }): never // prettier-ignore -export type TPick = +export type TPick = T extends TComposite ? TComposite> : T extends TIntersect ? TIntersect> : T extends TUnion ? TUnion> : diff --git a/src/value/create.ts b/src/value/create.ts index f6f63b6..dace6a6 100644 --- a/src/value/create.ts +++ b/src/value/create.ts @@ -44,7 +44,7 @@ export class ValueCreateNeverTypeError extends Error { } export class ValueCreateIntersectTypeError extends Error { constructor(public readonly schema: Types.TSchema) { - super('ValueCreate: Can only create values for intersected objects and non-varying primitive types. Consider using a default value.') + super('ValueCreate: Intersect produced invalid value. Consider using a default value.') } } export class ValueCreateTempateLiteralTypeError extends Error { @@ -152,7 +152,13 @@ export namespace ValueCreate { if ('default' in schema) { return schema.default } else { - const value = schema.type === 'object' ? schema.allOf.reduce((acc, schema) => ({ ...acc, ...(Visit(schema, references) as any) }), {}) : schema.allOf.reduce((_, schema) => Visit(schema, references), undefined as any) + // Note: The best we can do here is attempt to instance each sub type and apply through object assign. For non-object + // sub types, we just escape the assignment and just return the value. In the latter case, this is typically going to + // be a consequence of an illogical intersection. + const value = schema.allOf.reduce((acc, schema) => { + const next = Visit(schema, references) as any + return typeof next === 'object' ? { ...acc, ...next } : next + }, {}) if (!ValueCheck.Check(schema, references, value)) throw new ValueCreateIntersectTypeError(schema) return value } diff --git a/test/runtime/type/guard/index.ts b/test/runtime/type/guard/index.ts index d9d8086..d409061 100644 --- a/test/runtime/type/guard/index.ts +++ b/test/runtime/type/guard/index.ts @@ -11,6 +11,7 @@ import './function' import './integer' import './literal' import './intersect' +import './keyof' import './not' import './null' import './number' diff --git a/test/runtime/type/guard/keyof.ts b/test/runtime/type/guard/keyof.ts new file mode 100644 index 0000000..d1f6e9e --- /dev/null +++ b/test/runtime/type/guard/keyof.ts @@ -0,0 +1,59 @@ +import { TypeGuard } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +describe('type/guard/TKeyOf', () => { + it('Should guard for keyof TObject', () => { + const T = Type.Object({ + x: Type.Number(), + y: Type.Number(), + }) + const K = Type.KeyOf(T) + Assert.deepEqual(TypeGuard.TUnion(K), true) + Assert.deepEqual(TypeGuard.TLiteral(K.anyOf[0]), true) + Assert.deepEqual(TypeGuard.TLiteral(K.anyOf[1]), true) + }) + it('Should guard for keyof TRecursive', () => { + const T = Type.Recursive((Self) => + Type.Object({ + x: Type.Number(), + y: Type.Array(Self), + }), + ) + const K = Type.KeyOf(T) + Assert.deepEqual(TypeGuard.TUnion(K), true) + Assert.deepEqual(TypeGuard.TLiteral(K.anyOf[0]), true) + Assert.deepEqual(TypeGuard.TLiteral(K.anyOf[1]), true) + }) + it('Should guard for keyof TIntersect', () => { + const T = Type.Intersect([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]) + const K = Type.KeyOf(T) + Assert.deepEqual(TypeGuard.TUnion(K), true) + Assert.deepEqual(TypeGuard.TLiteral(K.anyOf[0]), true) + Assert.deepEqual(TypeGuard.TLiteral(K.anyOf[1]), true) + }) + it('Should guard for keyof TUnion', () => { + const T = Type.Union([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.Number(), + }), + ]) + const K = Type.KeyOf(T) + Assert.deepEqual(TypeGuard.TNever(K), true) + }) + it('Should guard for keyof TNull', () => { + const T = Type.Null() + const K = Type.KeyOf(T) + Assert.deepEqual(TypeGuard.TNever(K), true) + }) +}) diff --git a/test/runtime/value/create/intersect.ts b/test/runtime/value/create/intersect.ts index 344ad5b..f007480 100644 --- a/test/runtime/value/create/intersect.ts +++ b/test/runtime/value/create/intersect.ts @@ -37,4 +37,26 @@ describe('value/create/Intersect', () => { const R = Value.Create(T) Assert.deepEqual(R, 'hello') }) + it('Should create from nested intersection', () => { + const T = Type.Intersect([ + Type.Object({ + x: Type.Number({ default: 1 }), + }), + Type.Intersect([ + Type.Object({ + y: Type.Number({ default: 2 }), + }), + Type.Object({ + z: Type.Number({ default: 3 }), + }), + ]), + ]) + const R = Value.Create(T) + Assert.deepEqual(R, { x: 1, y: 2, z: 3 }) + }) + it('Should create non varying primitive', () => { + const T = Type.Intersect([Type.Number(), Type.Number(), Type.Number()]) + const R = Value.Create(T) + Assert.deepEqual(R, 0) + }) }) diff --git a/test/static/keyof.ts b/test/static/keyof.ts index ff353b7..bda75e8 100644 --- a/test/static/keyof.ts +++ b/test/static/keyof.ts @@ -79,3 +79,15 @@ import { Type } from '@sinclair/typebox' Expect(K2).ToInfer<'y' | 'z'>() } } +{ + const T = Type.Recursive((Self) => + Type.Object({ + a: Type.String(), + b: Type.String(), + c: Type.String(), + d: Type.Array(Self), + }), + ) + const K = Type.KeyOf(T) + Expect(K).ToInfer<'a' | 'b' | 'c' | 'd'>() +}