diff --git a/infer.types.ts b/infer.types.ts index 7212ae2..1dfba2d 100644 --- a/infer.types.ts +++ b/infer.types.ts @@ -623,3 +623,35 @@ assert<[string, boolean]>(T2); // @ts-expect-error; STRING is not in ENUM assert([STRING, true]); + +// --- +// COMPOSITIONS +// --- + +let not1 = t.not(t.string()); +type NOT1 = t.Infer; +declare let NOT1: NOT1; +assert(NOT1); + +let any1 = t.any(t.string(), t.number()); +type ANY1 = t.Infer; +declare let ANY1: ANY1; +assert(ANY1); +assert(STRING); +assert(NUMBER); + +// @ts-expect-error; must all be string +let all1 = t.all(t.string(), t.number()); +type ALL1 = t.Infer; +declare let ALL1: ALL1; +assert(ALL1); +assert(STRING); + +let one1 = t.one(t.string(), t.number(), t.boolean()); +type ONE1 = t.Infer; +declare let ONE1: ONE1; +assert(ONE1); +assert(STRING); +assert(NUMBER); +assert(false); +assert(true); diff --git a/mod.test.ts b/mod.test.ts index 319b19c..ecb77dd 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -519,3 +519,101 @@ describe('Readonly', () => { }); }); }); + +// --- + +describe('t.not', () => { + it('should be a function', () => { + assert(typeof t.not === 'function'); + }); + + it('should be JSON schema', () => { + let output = t.not( + t.string(), + ); + + assertEquals(output, { + not: { + type: 'string', + }, + }); + }); +}); + +describe('t.one', () => { + it('should be a function', () => { + assert(typeof t.one === 'function'); + }); + + it('should be JSON schema', () => { + let output = t.one( + t.number({ multipleOf: 5 }), + t.number({ multipleOf: 3 }), + ); + + assertEquals(output, { + oneOf: [ + { + type: 'number', + multipleOf: 5, + }, + { + type: 'number', + multipleOf: 3, + }, + ], + }); + }); +}); + +describe('t.any', () => { + it('should be a function', () => { + assert(typeof t.any === 'function'); + }); + + it('should be JSON schema', () => { + let output = t.any( + t.string({ maxLength: 5 }), + t.number({ minimum: 0 }), + ); + + assertEquals(output, { + anyOf: [ + { + type: 'string', + maxLength: 5, + }, + { + type: 'number', + minimum: 0, + }, + ], + }); + }); +}); + +describe('t.all', () => { + it('should be a function', () => { + assert(typeof t.all === 'function'); + }); + + it('should be JSON schema', () => { + let output = t.all( + t.number({ maximum: 5 }), + t.number({ minimum: 0 }), + ); + + assertEquals(output, { + allOf: [ + { + type: 'number', + maximum: 5, + }, + { + type: 'number', + minimum: 0, + }, + ], + }); + }); +}); diff --git a/mod.ts b/mod.ts index f80e97b..59de485 100644 --- a/mod.ts +++ b/mod.ts @@ -1,3 +1,5 @@ +// import { Union } from 'npm:@sinclair/typebox'; + /** * @module */ @@ -8,6 +10,8 @@ type Prettify = // deno-lint-ignore ban-types & {}; +type Union = T extends [] ? never : T extends [Type] ? T[0] : Union; + // deno-fmt-ignore /** * Infer the TypeScript type(s) information from a {@link Type} definition. @@ -37,6 +41,10 @@ export type Infer = : T extends _enum ? E : T extends _tuple ? Infer : T extends _array ? Infer[] + // compositions + : T extends _not ? unknown + : T extends _any | _one ? Infer + : T extends _all ? Infer // read out values : T extends [infer A, ...infer B] ? [Infer, ...Infer] @@ -603,8 +611,108 @@ function _object< return o; } +// compositions + +/** + * An `anyOf` (OR) composition. + * + * [Reference](https://json-schema.org/understanding-json-schema/reference/combining#anyOf) + */ +type _any = { + anyOf: T[]; +}; + +/** + * Defines an `anyOf` (OR) composition. + * + * Declares that the field may be valid against *any* of the subschemas. + * + * > [!IMPORTANT] + * > This is NOT the TypeScript `any` keyword! + * + * ```ts + * let _ = t.any( + * t.string({ maxLength: 5 }), + * t.number({ minimum: 0 }), + * ); + * //-> PASS: `"short"` or `12` + * //-> FAIL: `"too long"` or `-5` + * ``` + */ +function _any(...anyOf: T): _any { + return { anyOf }; +} + +/** + * An `oneOf` (XOR) composition. + * + * [Reference](https://json-schema.org/understanding-json-schema/reference/combining#oneOf) + */ +type _one = { + oneOf: T[]; +}; + +/** + * Defines an `oneOf` composition. + * + * Declares that a field must be valid against *exactly one* of the subschemas. + * + * ```ts + * let _ = t.one( + * t.number({ multipleOf: 5 }), + * t.number({ multipleOf: 3 }), + * ); + * //-> PASS: `10` or `9` + * //-> FAIL: `15` or `2` + * ``` + */ +function _one(...oneOf: T): _one { + return { oneOf }; +} + +/** + * A `not` composition. + * + * [Reference](https://json-schema.org/understanding-json-schema/reference/combining#not) + */ +type _not = { + not: T; +}; + +/** + * Defines a `not` composition (NOT). + * + * Declares that a field must *not* be valid against the schema. + */ +function _not(not: T): _not { + return { not }; +} + +/** + * An `allOf` (AND) composition. + * + * [Reference](https://json-schema.org/understanding-json-schema/reference/combining#allOf) + */ +type _all = { + allOf: T[]; +}; + +/** + * Defines an `allOf` composition (AND). + * + * Declares that a field must be valid against **all** of the subschemas. + */ +function _all(...allOf: T[]): _all { + return { allOf }; +} + // deno-fmt-ignore export { + // composites + _all as all, + _any as any, + _one as one, + _not as not, // modifiers _optional as optional, _readonly as readonly,