diff --git a/changelog/0.29.0.md b/changelog/0.29.0.md new file mode 100644 index 0000000..e4cca9f --- /dev/null +++ b/changelog/0.29.0.md @@ -0,0 +1,108 @@ +## [0.29.0](https://www.npmjs.com/package/@sinclair/typebox/v/0.29.0) + +## Overview + +Revision 0.29.0 makes a minor interface and schema representation change to the `Type.Not` type. This revision also includes a fix for indexed access types on TypeScript 5.1.6. + +As this revision constitutes a breaking representation change for `Type.Not`, a minor semver revision is required. + +## Type.Not Representation Change + +The `Type.Not` was first introduced in Revision 0.26.0. This type accepted two arguments, the first is the `not` type, the second is the `allowed` type. In 0.26.0, TypeBox would treat the `allowed` type as the inferred type with the schema represented in the following form. + +### 0.26.0 + +```typescript +// allow all numbers except the number 42 +// +const T = Type.Not(Type.Literal(42), Type.Number()) +// ^ ^ +// not type allowed type + +// represented as +// +const T = { + allOf: [ + { not: { const: 42 } }, + { type: 'number' } + ] +} + +// inferred as +// +type T = Static // type T = number +``` +In 0.26.0. the rationale for the second `allowed` argument was provide a correct static type to infer, where one could describe what the type wasn't on the first and what it was on the second (with inference of operating on the second argument). This approach was to echo possible suggestions for negated type syntax in TypeScript. + +```typescript +type T = number & not 42 // not actual typescript syntax! +``` + +### 0.29.0 + +Revision 0.29.0 changes the `Type.Not` type to take a single `not` argument only. This type statically infers as `unknown` + +```typescript +// allow all types except the literal number 42 +// +const T = Type.Not(Type.Literal(42)) +// ^ +// not type + +// represented as +// +const T = { not: { const: 42 } } + +// inferred as +// +type T = Static // type T = unknown + +``` +### Upgrading to 0.29.0 + +In revision 0.29.0, you can express the 0.26.0 Not type via `Type.Intersect` which explicitly creates the `allOf` representation. The type inference works in this case as intersected `number & unknown` yields the most narrowed type (which is `number`) + +```typescript +// allow all numbers except the number 42 +// +const T = Type.Intersect([ Type.Not(Type.Literal(42)), Type.Number() ]) +// ^ ^ +// not type allowed type + +// represented as +// +const T = { + allOf: [ + { not: { const: 42 } }, + { type: 'number' } + ] +} +// inferred as +// +type T = Static // type T = number +``` +The 0.29.0 `Not` type properly represents the JSON Schema `not` keyword in its simplest form, as well as making better use of the type intersection narrowing capabilities of TypeScript with respect to inference. + +### Invert Not + +In revision 0.29.0, it is possible to invert the `Not` type. TypeBox will track each inversion and statically infer appropriately. + +```typescript +// not not string +// +const T = Type.Not(Type.Not(Type.String())) + +// represented as +// +const T = { + not: { + not: { + type: "string" + } + } +} + +// inferred as +// +type T = Static // type T = string +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 683b43e..98d18eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.28.20", + "version": "0.29.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.28.20", + "version": "0.29.0", "license": "MIT", "devDependencies": { "@sinclair/hammer": "^0.17.1", @@ -18,7 +18,7 @@ "chai": "^4.3.6", "mocha": "^9.2.2", "prettier": "^2.7.1", - "typescript": "^5.1.3" + "typescript": "^5.1.6" } }, "node_modules/@esbuild/linux-loong64": { @@ -1410,9 +1410,9 @@ } }, "node_modules/typescript": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", - "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2442,9 +2442,9 @@ "dev": true }, "typescript": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.3.tgz", - "integrity": "sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true }, "uri-js": { diff --git a/package.json b/package.json index d6fb9bf..6cc25a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.28.20", + "version": "0.29.0", "description": "JSONSchema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", @@ -42,6 +42,6 @@ "chai": "^4.3.6", "mocha": "^9.2.2", "prettier": "^2.7.1", - "typescript": "^5.1.3" + "typescript": "^5.1.6" } } diff --git a/readme.md b/readme.md index 7d7b48f..1ff66fd 100644 --- a/readme.md +++ b/readme.md @@ -84,6 +84,7 @@ License MIT - [Conditional](#types-conditional) - [Template Literal](#types-template-literal) - [Indexed](#types-indexed) + - [Not](#types-not) - [Rest](#types-rest) - [Guards](#types-guards) - [Unsafe](#types-unsafe) @@ -353,20 +354,11 @@ The following table lists the Standard TypeBox types. These types are fully comp │ │ │ } │ │ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ -│ const T = Type.Not( | type T = string │ const T = { │ -| Type.Union([ │ │ allOf: [{ │ -│ Type.Literal('x'), │ │ not: { │ -│ Type.Literal('y'), │ │ anyOf: [ │ -│ Type.Literal('z') │ │ { const: 'x' }, │ -│ ]), │ │ { const: 'y' }, │ -│ Type.String() │ │ { const: 'z' } │ -│ ) │ │ ] │ -│ │ │ } │ -│ │ │ }, { │ -│ │ │ type: 'string' │ -│ │ │ }] │ +│ const T = Type.Not( | type T = unknown │ const T = { │ +│ Type.String() │ │ not: { │ +│ ) │ │ type: 'string' │ +│ │ │ } │ │ │ │ } │ -│ │ │ │ ├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤ │ const T = Type.Extends( │ type T = │ const T = { │ │ Type.String(), │ string extends number │ const: false, │ @@ -650,7 +642,7 @@ const T = Type.String({ // const T = { }) // format: 'email' // } -// Mumber must be a multiple of 2 +// Number must be a multiple of 2 const T = Type.Number({ // const T = { multipleOf: 2 // type: 'number', }) // multipleOf: 2 @@ -892,6 +884,49 @@ const C = Type.Index(T, Type.KeyOf(T)) // const C = { // } ``` + + +### Not Types + +Not types are supported with `Type.Not`. This type represents the JSON Schema `not` keyword and will statically infer as `unknown`. Note that negated (or not) types are not supported in TypeScript, but can still be partially expressed by interpreting `not` as the broad type `unknown`. When used with intersect types, the Not type can create refined assertion rules for types by leveraging TypeScript's ability to narrow from `unknown` to an intended type through intersection. + +For example, consider a type which is `number` but not `1 | 2 | 3` and where the static type would still technically be a `number`. The following shows a pseudo TypeScript example using `not` followed by the TypeBox implementation. + +```typescript +// Pseudo TypeScript + +type T = number & not (1 | 2 | 3) // allow all numbers except 1, 2, 3 + +// TypeBox + +const T = Type.Intersect([ // const T = { + Type.Number(), // allOf: [ + Type.Not(Type.Union([ // { type: "number" }, + Type.Literal(1), // { + Type.Literal(2), // not: { + Type.Literal(3) // anyOf: [ + ])) // { const: 1, type: "number" }, +]) // { const: 2, type: "number" }, + // { const: 3, type: "number" } + // ] + // } + // } + // ] + // } + +type T = Static // evaluates as: + // + // type T = (number & (not (1 | 2 | 3))) + // type T = (number & (unknown)) + // type T = (number) +``` + +The Not type can be used with constraints to define schematics for types that would otherwise be difficult to express. +```typescript +const Even = Type.Number({ multipleOf: 2 }) + +const Odd = Type.Intersect([Type.Number(), Type.Not(Even)]) +``` ### Rest Types @@ -1420,29 +1455,25 @@ TypeSystem.AllowNaN = true ## Workbench -TypeBox offers a small web based code generation tool that can be used to convert TypeScript types into TypeBox type definitions as well as a variety of other formats. +TypeBox offers a web based code generation tool that can be used to convert TypeScript types into TypeBox types as well as a variety of other runtime type representations. [Workbench Link Here](https://sinclairzx81.github.io/typebox-workbench/) -
- - - -
- ## Ecosystem -The following is a list of community packages that provide general tooling and framework support for TypeBox. +The following is a list of community packages that provide general tooling and framework integration support for TypeBox. | Package | Description | | ------------- | ------------- | | [elysia](https://github.com/elysiajs/elysia) | Fast and friendly Bun web framework | | [fastify-type-provider-typebox](https://github.com/fastify/fastify-type-provider-typebox) | Fastify TypeBox integration with the Fastify Type Provider | +| [feathersjs](https://github.com/feathersjs/feathers) | The API and real-time application framework | | [fetch-typebox](https://github.com/erfanium/fetch-typebox) | Drop-in replacement for fetch that brings easy integration with TypeBox | | [schema2typebox](https://github.com/xddq/schema2typebox) | Creating TypeBox code from JSON schemas | -| [ts2typebox](https://github.com/xddq/ts2typebox) | Creating TypeBox code from Typescript types | +| [ts2typebox](https://github.com/xddq/ts2typebox) | Creating TypeBox code from Typescript types | +| [typebox-validators](https://github.com/jtlapp/typebox-validators) | Advanced validators supporting discriminated and heterogeneous unions | diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 4ba3c8e..49e8ddb 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -245,9 +245,8 @@ export namespace TypeCompiler { yield `false` } function* Not(schema: Types.TNot, references: Types.TSchema[], value: string): IterableIterator { - const left = CreateExpression(schema.allOf[0].not, references, value) - const right = CreateExpression(schema.allOf[1], references, value) - yield `!${left} && ${right}` + const expression = CreateExpression(schema.not, references, value) + yield `(!${expression})` } function* Null(schema: Types.TNull, references: Types.TSchema[], value: string): IterableIterator { yield `(${value} === null)` diff --git a/src/errors/errors.ts b/src/errors/errors.ts index 6c7203e..aa5fe28 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -305,10 +305,9 @@ export namespace ValueErrors { yield { type: ValueErrorType.Never, schema, path, value, message: `Value cannot be validated` } } function* Not(schema: Types.TNot, references: Types.TSchema[], path: string, value: any): IterableIterator { - if (Visit(schema.allOf[0].not, references, path, value).next().done === true) { + if (Visit(schema.not, references, path, value).next().done === true) { yield { type: ValueErrorType.Not, schema, path, value, message: `Value should not validate` } } - yield* Visit(schema.allOf[1], references, path, value) } function* Null(schema: Types.TNull, references: Types.TSchema[], path: string, value: any): IterableIterator { if (!(value === null)) { diff --git a/src/typebox.ts b/src/typebox.ts index de871f3..f5c04c6 100644 --- a/src/typebox.ts +++ b/src/typebox.ts @@ -313,21 +313,13 @@ export type TIndexRestMany = K extends [infer L, ...infer R] ? [TIndexType>, ...TIndexRestMany>] : [] // prettier-ignore -export type TIndexReduce = - T extends TRecursive ? TIndexReduce : +export type TIndex = + T extends TRecursive ? TIndex : T extends TIntersect ? UnionType>> : T extends TUnion ? UnionType>> : T extends TObject ? UnionType>> : T extends TTuple ? UnionType>> : TNever -// prettier-ignore -export type TIndex = - [T, K] extends [TTuple, TNumber] ? UnionType> : - [T, K] extends [TArray, TNumber] ? AssertType : - K extends TTemplateLiteral ? TIndexReduce> : - K extends TUnion[]> ? TIndexReduce> : - K extends TLiteral ? TIndexReduce : - TNever // -------------------------------------------------------------------------- // TInteger // -------------------------------------------------------------------------- @@ -393,10 +385,10 @@ export interface TNever extends TSchema { // -------------------------------------------------------------------------- // TNot // -------------------------------------------------------------------------- -export interface TNot extends TSchema { +export interface TNot extends TSchema { [Kind]: 'Not' - static: Static - allOf: [{ not: Not }, T] + static: T extends TNot ? Static : unknown + not: T } // -------------------------------------------------------------------------- // TNull @@ -1063,11 +1055,7 @@ export namespace TypeGuard { return ( TKind(schema) && schema[Kind] === 'Not' && - IsArray(schema.allOf) && - schema.allOf.length === 2 && - IsObject(schema.allOf[0]) && - TSchema(schema.allOf[0].not) && - TSchema(schema.allOf[1]) + TSchema(schema.not) ) } /** Returns true if the given schema is TNull */ @@ -1354,8 +1342,7 @@ export namespace ExtendsUndefined { export function Check(schema: TSchema): boolean { if (schema[Kind] === 'Undefined') return true if (schema[Kind] === 'Not') { - const not = schema as TNot - return Check(not.allOf[1]) + return !Check(schema.not) } if (schema[Kind] === 'Intersect') { const intersect = schema as TIntersect @@ -1551,6 +1538,18 @@ export namespace TypeExtends { return TypeExtendsResult.True } // -------------------------------------------------------------------------- + // Not + // -------------------------------------------------------------------------- + function ResolveNot(schema: T): TUnknown | TNot['not'] { + let [current, depth]: [TSchema, number] = [schema, 0] + while (true) { + if (!TypeGuard.TNot(current)) break + current = current.not + depth += 1 + } + return depth % 2 === 0 ? current : Type.Unknown() + } + // -------------------------------------------------------------------------- // Null // -------------------------------------------------------------------------- function Null(left: TNull, right: TSchema) { @@ -1858,6 +1857,9 @@ export namespace TypeExtends { return TypeGuard.TVoid(right) ? TypeExtendsResult.True : TypeExtendsResult.False } function Visit(left: TSchema, right: TSchema): TypeExtendsResult { + // Not Unwrap + if (TypeGuard.TNot(left)) return Visit(ResolveNot(left), right) + if (TypeGuard.TNot(right)) return Visit(left, ResolveNot(right)) // Template Literal Union Unwrap if (TypeGuard.TTemplateLiteral(left)) return Visit(TemplateLiteralResolver.Resolve(left), right) if (TypeGuard.TTemplateLiteral(right)) return Visit(left, TemplateLiteralResolver.Resolve(right)) @@ -2441,9 +2443,19 @@ export class StandardTypeBuilder extends TypeBuilder { } } /** `[Standard]` Returns indexed property types for the given keys */ - public Index)[]>(schema: T, keys: [...K], options?: SchemaOptions): TIndexReduce> + public Index(schema: T, keys: K, options?: SchemaOptions): UnionType> /** `[Standard]` Returns indexed property types for the given keys */ - public Index(schema: T, key: K, options?: SchemaOptions): TIndex + public Index(schema: T, keys: K, options?: SchemaOptions): AssertType + /** `[Standard]` Returns indexed property types for the given keys */ + public Index(schema: T, keys: K, options?: SchemaOptions): TIndex> + /** `[Standard]` Returns indexed property types for the given keys */ + public Index>(schema: T, keys: K, options?: SchemaOptions): TIndex + /** `[Standard]` Returns indexed property types for the given keys */ + public Index)[]>(schema: T, keys: [...K], options?: SchemaOptions): TIndex> + /** `[Standard]` Returns indexed property types for the given keys */ + public Index[]>>(schema: T, keys: K, options?: SchemaOptions): TIndex> + /** `[Standard]` Returns indexed property types for the given keys */ + public Index(schema: T, key: K, options?: SchemaOptions): TSchema /** `[Standard]` Returns indexed property types for the given keys */ public Index(schema: TSchema, unresolved: any, options: SchemaOptions = {}): any { if (TypeGuard.TArray(schema) && TypeGuard.TNumber(unresolved)) { @@ -2508,9 +2520,9 @@ export class StandardTypeBuilder extends TypeBuilder { public Never(options: SchemaOptions = {}): TNever { return this.Create({ ...options, [Kind]: 'Never', not: {} }) } - /** `[Standard]` Creates a Not type. The first argument is the disallowed type, the second is the allowed. */ - public Not(not: N, schema: T, options?: SchemaOptions): TNot { - return this.Create({ ...options, [Kind]: 'Not', allOf: [{ not: TypeClone.Clone(not, {}) }, TypeClone.Clone(schema, {})] }) + /** `[Standard]` Creates a Not type */ + public Not(not: T, options?: SchemaOptions): TNot { + return this.Create({ ...options, [Kind]: 'Not', not }) } /** `[Standard]` Creates a Null type */ public Null(options: SchemaOptions = {}): TNull { diff --git a/src/value/cast.ts b/src/value/cast.ts index 66bb2ab..1ff3db9 100644 --- a/src/value/cast.ts +++ b/src/value/cast.ts @@ -178,7 +178,7 @@ export namespace ValueCast { throw new ValueCastNeverTypeError(schema) } function Not(schema: Types.TNot, references: Types.TSchema[], value: any): any { - return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema.allOf[1], references) + return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) } function Null(schema: Types.TNull, references: Types.TSchema[], value: any): any { return ValueCheck.Check(schema, references, value) ? value : ValueCreate.Create(schema, references) diff --git a/src/value/check.ts b/src/value/check.ts index 40c2fd5..a0458cb 100644 --- a/src/value/check.ts +++ b/src/value/check.ts @@ -201,7 +201,7 @@ export namespace ValueCheck { return false } function Not(schema: Types.TNot, references: Types.TSchema[], value: any): boolean { - return !Visit(schema.allOf[0].not, references, value) && Visit(schema.allOf[1], references, value) + return !Visit(schema.not, references, value) } function Null(schema: Types.TNull, references: Types.TSchema[], value: any): boolean { return value === null diff --git a/src/value/create.ts b/src/value/create.ts index ae2e9b1..66973e3 100644 --- a/src/value/create.ts +++ b/src/value/create.ts @@ -42,6 +42,11 @@ export class ValueCreateNeverTypeError extends Error { super('ValueCreate: Never types cannot be created') } } +export class ValueCreateNotTypeError extends Error { + constructor(public readonly schema: Types.TSchema) { + super('ValueCreate: Not types must have a default value') + } +} export class ValueCreateIntersectTypeError extends Error { constructor(public readonly schema: Types.TSchema) { super('ValueCreate: Intersect produced invalid value. Consider using a default value.') @@ -182,7 +187,7 @@ export namespace ValueCreate { if ('default' in schema) { return schema.default } else { - return Visit(schema.allOf[1], references) + throw new ValueCreateNotTypeError(schema) } } function Null(schema: Types.TNull, references: Types.TSchema[]): any { diff --git a/test/runtime/compiler/not.ts b/test/runtime/compiler/not.ts index 27a07c5..144b31d 100644 --- a/test/runtime/compiler/not.ts +++ b/test/runtime/compiler/not.ts @@ -2,37 +2,44 @@ import { Type } from '@sinclair/typebox' import { Ok, Fail } from './validate' describe('type/compiler/Not', () => { - it('Should validate with not number', () => { - const T = Type.Not(Type.Number(), Type.String()) + it('Should validate not number', () => { + const T = Type.Not(Type.Number()) Fail(T, 1) - Ok(T, 'A') + Ok(T, '1') }) - it('Should validate with union left', () => { + it('Should validate not not number', () => { + const T = Type.Not(Type.Not(Type.Number())) + Ok(T, 1) + Fail(T, '1') + }) + it('Should validate not union', () => { // prettier-ignore const T = Type.Not(Type.Union([ Type.Literal('A'), Type.Literal('B'), Type.Literal('C') - ]), Type.String()) + ])) Fail(T, 'A') Fail(T, 'B') Fail(T, 'C') Ok(T, 'D') }) - it('Should validate with union right', () => { - // prettier-ignore - const T = Type.Not(Type.Number(), Type.Union([ - Type.String(), - Type.Boolean() - ])) - Fail(T, 1) - Ok(T, 'A') - Ok(T, true) - }) - it('Should not validate with symmetric left right', () => { - // prettier-ignore - const T = Type.Not(Type.Number(), Type.Number()) - Fail(T, 1) - Fail(T, true) // not a number, but not a number either? + it('Should validate not object intersection', () => { + const T = Type.Intersect([ + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + Type.Object({ + x: Type.Not(Type.Literal(0)), + y: Type.Not(Type.Literal(0)), + z: Type.Not(Type.Literal(0)), + }), + ]) + Fail(T, { x: 0, y: 0, z: 0 }) + Fail(T, { x: 1, y: 0, z: 0 }) + Fail(T, { x: 1, y: 1, z: 0 }) + Ok(T, { x: 1, y: 1, z: 1 }) }) }) diff --git a/test/runtime/compiler/object.ts b/test/runtime/compiler/object.ts index 47a59a6..5b30793 100644 --- a/test/runtime/compiler/object.ts +++ b/test/runtime/compiler/object.ts @@ -7,7 +7,7 @@ describe('type/compiler/Object', () => { // ----------------------------------------------------- it('Should handle extends undefined check 1', () => { const T = Type.Object({ - A: Type.Not(Type.Number(), Type.Undefined()), + A: Type.Not(Type.Number()), B: Type.Union([Type.Number(), Type.Undefined()]), C: Type.Intersect([Type.Undefined(), Type.Undefined()]), }) @@ -20,7 +20,7 @@ describe('type/compiler/Object', () => { // https://github.com/sinclairzx81/typebox/issues/437 it('Should handle extends undefined check 2', () => { const T = Type.Object({ - A: Type.Not(Type.Null(), Type.Undefined()), + A: Type.Not(Type.Null()), }) Ok(T, { A: undefined }) Fail(T, { A: null }) diff --git a/test/runtime/schema/not.ts b/test/runtime/schema/not.ts index c8ec36f..491de05 100644 --- a/test/runtime/schema/not.ts +++ b/test/runtime/schema/not.ts @@ -2,37 +2,44 @@ import { Type } from '@sinclair/typebox' import { Ok, Fail } from './validate' describe('type/schema/Not', () => { - it('Should validate with not number', () => { - const T = Type.Not(Type.Number(), Type.String()) + it('Should validate not number', () => { + const T = Type.Not(Type.Number()) Fail(T, 1) - Ok(T, 'A') + Ok(T, '1') }) - it('Should validate with union left', () => { + it('Should validate not not number', () => { + const T = Type.Not(Type.Not(Type.Number())) + Ok(T, 1) + Fail(T, '1') + }) + it('Should validate not union', () => { // prettier-ignore const T = Type.Not(Type.Union([ Type.Literal('A'), Type.Literal('B'), Type.Literal('C') - ]), Type.String()) + ])) Fail(T, 'A') Fail(T, 'B') Fail(T, 'C') Ok(T, 'D') }) - it('Should validate with union right', () => { - // prettier-ignore - const T = Type.Not(Type.Number(), Type.Union([ - Type.String(), - Type.Boolean() - ])) - Fail(T, 1) - Ok(T, 'A') - Ok(T, true) - }) - it('Should not validate with symmetric left right', () => { - // prettier-ignore - const T = Type.Not(Type.Number(), Type.Number()) - Fail(T, 1) - Fail(T, true) // not a number, but not a number either? + it('Should validate not object intersection', () => { + const T = Type.Intersect([ + Type.Object({ + x: Type.Number(), + y: Type.Number(), + z: Type.Number(), + }), + Type.Object({ + x: Type.Not(Type.Literal(0)), + y: Type.Not(Type.Literal(0)), + z: Type.Not(Type.Literal(0)), + }), + ]) + Fail(T, { x: 0, y: 0, z: 0 }) + Fail(T, { x: 1, y: 0, z: 0 }) + Fail(T, { x: 1, y: 1, z: 0 }) + Ok(T, { x: 1, y: 1, z: 1 }) }) }) diff --git a/test/runtime/type/extends/index.ts b/test/runtime/type/extends/index.ts index 1bd1dc9..be6eed0 100644 --- a/test/runtime/type/extends/index.ts +++ b/test/runtime/type/extends/index.ts @@ -7,6 +7,7 @@ import './date' import './function' import './integer' import './literal' +import './not' import './null' import './number' import './object' diff --git a/test/runtime/type/extends/not.ts b/test/runtime/type/extends/not.ts new file mode 100644 index 0000000..be5bbfa --- /dev/null +++ b/test/runtime/type/extends/not.ts @@ -0,0 +1,130 @@ +import { TypeExtends, TypeExtendsResult } from '@sinclair/typebox' +import { Type } from '@sinclair/typebox' +import { Assert } from '../../assert/index' + +// --------------------------------------------------------------------------- +// Note: Not is equivalent to Unknown with the exception of nested negation. +// --------------------------------------------------------------------------- +describe('type/extends/Not', () => { + // --------------------------------------------------------------------------- + // Nested + // --------------------------------------------------------------------------- + it('Should extend with nested negation', () => { + const T1 = Type.String() + const T2 = Type.Not(T1) + const T3 = Type.Not(T2) + const T4 = Type.Not(T3) + const T5 = Type.Not(T4) + + const R1 = TypeExtends.Extends(T1, Type.String()) + const R2 = TypeExtends.Extends(T2, Type.String()) + const R3 = TypeExtends.Extends(T3, Type.String()) + const R4 = TypeExtends.Extends(T4, Type.String()) + const R5 = TypeExtends.Extends(T5, Type.String()) + + Assert.isEqual(R1, TypeExtendsResult.True) + Assert.isEqual(R2, TypeExtendsResult.False) + Assert.isEqual(R3, TypeExtendsResult.True) + Assert.isEqual(R4, TypeExtendsResult.False) + Assert.isEqual(R5, TypeExtendsResult.True) + }) + + // --------------------------------------------------------------------------- + // Not as Unknown Tests + // --------------------------------------------------------------------------- + it('Should extend Any', () => { + type T = unknown extends any ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Any()) + Assert.isEqual(R, TypeExtendsResult.True) + }) + it('Should extend Unknown', () => { + type T = unknown extends unknown ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Unknown()) + Assert.isEqual(R, TypeExtendsResult.True) + }) + it('Should extend String', () => { + type T = unknown extends string ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.String()) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Boolean', () => { + type T = unknown extends boolean ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Boolean()) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Number', () => { + type T = unknown extends number ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Number()) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Integer', () => { + type T = unknown extends number ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Integer()) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Array 1', () => { + type T = unknown extends Array ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Array(Type.Any())) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Array 2', () => { + type T = unknown extends Array ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Array(Type.String())) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Tuple', () => { + type T = unknown extends [number, number] ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Tuple([Type.Number(), Type.Number()])) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Object 1', () => { + type T = unknown extends {} ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Object({})) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Object 2', () => { + type T = unknown extends { a: number } ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Object({ a: Type.Number() })) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Union 1', () => { + type T = unknown extends number | string ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Union([Type.Number(), Type.String()])) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Union 2', () => { + type T = unknown extends any | number ? 1 : 2 // 1 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Union([Type.Any(), Type.String()])) + Assert.isEqual(R, TypeExtendsResult.True) + }) + it('Should extend Union 3', () => { + type T = unknown extends unknown | number ? 1 : 2 // 1 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Union([Type.Unknown(), Type.String()])) + Assert.isEqual(R, TypeExtendsResult.True) + }) + it('Should extend Union 4', () => { + type T = unknown extends unknown | any ? 1 : 2 // 1 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Union([Type.Unknown(), Type.String()])) + Assert.isEqual(R, TypeExtendsResult.True) + }) + it('Should extend Null', () => { + type T = unknown extends null ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Null()) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Undefined', () => { + type T = unknown extends undefined ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Undefined()) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Void', () => { + type T = unknown extends void ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Void()) + Assert.isEqual(R, TypeExtendsResult.False) + }) + it('Should extend Date', () => { + type T = unknown extends Date ? 1 : 2 + const R = TypeExtends.Extends(Type.Not(Type.Number()), Type.Date()) + Assert.isEqual(R, TypeExtendsResult.False) + }) +}) diff --git a/test/runtime/type/guard/not.ts b/test/runtime/type/guard/not.ts index b5a8bf0..1bb7c2c 100644 --- a/test/runtime/type/guard/not.ts +++ b/test/runtime/type/guard/not.ts @@ -4,20 +4,16 @@ import { Assert } from '../../assert/index' describe('type/guard/TNot', () => { it('Should guard for TNot', () => { - const R = TypeGuard.TNot(Type.Not(Type.String(), Type.String())) + const R = TypeGuard.TNot(Type.Not(Type.String())) Assert.isEqual(R, true) }) it('Should not guard for TNot 1', () => { - const R = TypeGuard.TNot(Type.Not(null as any, Type.String())) - Assert.isEqual(R, false) - }) - it('Should not guard for TNot 2', () => { - const R = TypeGuard.TNot(Type.Not(Type.String(), null as any)) + const R = TypeGuard.TNot(Type.Not(null as any)) Assert.isEqual(R, false) }) it('Should not guard for TNot with invalid $id', () => { // @ts-ignore - const R = TypeGuard.TNot(Type.Not(Type.String(), Type.String()), { $id: 1 }) + const R = TypeGuard.TNot(Type.Not(Type.String()), { $id: 1 }) Assert.isEqual(R, true) }) }) diff --git a/test/runtime/value/cast/not.ts b/test/runtime/value/cast/not.ts index 0fc2d1e..e0713d4 100644 --- a/test/runtime/value/cast/not.ts +++ b/test/runtime/value/cast/not.ts @@ -3,57 +3,50 @@ import { Type } from '@sinclair/typebox' import { Assert } from '../../assert/index' describe('value/cast/Not', () => { - const T = Type.Not(Type.String(), Type.Number()) - const E = 0 + const T = Type.Not(Type.String(), { default: 0 }) it('Should upcast from string', () => { const value = 'hello' const result = Value.Cast(T, value) - Assert.isEqual(result, E) + Assert.isEqual(result, 0) // default }) it('Should upcast from number', () => { const value = 0 const result = Value.Cast(T, value) - Assert.isEqual(result, E) + Assert.isEqual(result, value) }) it('Should upcast from boolean', () => { const value = true const result = Value.Cast(T, value) - Assert.isEqual(result, E) + Assert.isEqual(result, value) }) it('Should upcast from object', () => { const value = {} const result = Value.Cast(T, value) - Assert.isEqual(result, E) + Assert.isEqual(result, value) }) it('Should upcast from array', () => { const value = [1] const result = Value.Cast(T, value) - Assert.isEqual(result, E) + Assert.isEqual(result, value) }) it('Should upcast from undefined', () => { const value = undefined const result = Value.Cast(T, value) - Assert.isEqual(result, E) + Assert.isEqual(result, value) }) it('Should upcast from null', () => { const value = null const result = Value.Cast(T, value) - Assert.isEqual(result, E) + Assert.isEqual(result, value) }) it('Should upcast from date', () => { const value = new Date(100) const result = Value.Cast(T, value) - Assert.isEqual(result, E) + Assert.isEqual(result, value) }) it('Should preserve', () => { const value = 100 const result = Value.Cast(T, value) Assert.isEqual(result, 100) }) - it('Should not preserve when schema is illogical', () => { - const T = Type.Not(Type.Number(), Type.Number()) - const value = 100 - const result = Value.Cast(T, value) - Assert.isEqual(result, 0) - }) }) diff --git a/test/runtime/value/check/not.ts b/test/runtime/value/check/not.ts index abfc895..780d08c 100644 --- a/test/runtime/value/check/not.ts +++ b/test/runtime/value/check/not.ts @@ -30,10 +30,4 @@ describe('value/check/Not', () => { Assert.isEqual(Value.Check(T, 'A'), true) Assert.isEqual(Value.Check(T, true), true) }) - it('Should not validate with symmetric left right', () => { - // prettier-ignore - const T = Type.Not(Type.Number(), Type.Number()) - Assert.isEqual(Value.Check(T, 1), false) - Assert.isEqual(Value.Check(T, true), false) - }) }) diff --git a/test/runtime/value/create/not.ts b/test/runtime/value/create/not.ts index 433e855..9890372 100644 --- a/test/runtime/value/create/not.ts +++ b/test/runtime/value/create/not.ts @@ -3,18 +3,17 @@ import { Type } from '@sinclair/typebox' import { Assert } from '../../assert/index' describe('value/create/Not', () => { - it('Should create value', () => { - const T = Type.Not(Type.String(), Type.Number()) - const R = Value.Create(T) - Assert.isEqual(R, 0) + it('Should throw without default value', () => { + const T = Type.Not(Type.String()) + Assert.throws(() => Value.Create(T)) }) it('Should create value with default inner', () => { - const T = Type.Not(Type.String(), Type.Number({ default: 100 })) + const T = Type.Not(Type.String(), { default: 100 }) const R = Value.Create(T) Assert.isEqual(R, 100) }) it('Should create value with default outer', () => { - const T = Type.Not(Type.String(), Type.Number(), { default: 100 }) + const T = Type.Not(Type.String(), { default: 100 }) const R = Value.Create(T) Assert.isEqual(R, 100) }) diff --git a/test/static/not.ts b/test/static/not.ts index ea15715..aa8a74d 100644 --- a/test/static/not.ts +++ b/test/static/not.ts @@ -2,6 +2,10 @@ import { Expect } from './assert' import { Type } from '@sinclair/typebox' { - const T = Type.Not(Type.Number(), Type.String()) - Expect(T).ToInfer() + const T = Type.Not(Type.Number()) + Expect(T).ToInfer() +} +{ + const T = Type.Not(Type.Not(Type.Number())) + Expect(T).ToInfer() } diff --git a/typebox.png b/typebox.png index ed053e1..d38acd1 100644 Binary files a/typebox.png and b/typebox.png differ diff --git a/workbench.png b/workbench.png deleted file mode 100644 index 817b9d1..0000000 Binary files a/workbench.png and /dev/null differ