Revision 0.29.0 (#483)

This commit is contained in:
sinclairzx81
2023-07-02 22:37:54 +09:00
committed by GitHub
parent 3b6c9849bf
commit ebef7cc1ca
22 changed files with 431 additions and 146 deletions

108
changelog/0.29.0.md Normal file
View File

@@ -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<typeof T> // 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<typeof T> // 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<typeof T> // 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<typeof T> // type T = string
```

18
package-lock.json generated
View File

@@ -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": {

View File

@@ -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"
}
}

View File

@@ -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 = {
// }
```
<a name='types-not'></a>
### 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<typeof T> // 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)])
```
<a name='types-rest'></a>
### 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/)
<div align='center'>
<a href="https://sinclairzx81.github.io/typebox-workbench/"><img src="https://github.com/sinclairzx81/typebox/blob/master/workbench.png?raw=true" /></a>
</div>
<a name='ecosystem'></a>
## 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 |
<a name='benchmark'></a>

View File

@@ -245,9 +245,8 @@ export namespace TypeCompiler {
yield `false`
}
function* Not(schema: Types.TNot, references: Types.TSchema[], value: string): IterableIterator<string> {
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<string> {
yield `(${value} === null)`

View File

@@ -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<ValueError> {
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<ValueError> {
if (!(value === null)) {

View File

@@ -313,21 +313,13 @@ export type TIndexRestMany<T extends TSchema, K extends Key[]> =
K extends [infer L, ...infer R] ? [TIndexType<T, Assert<L, Key>>, ...TIndexRestMany<T, Assert<R, Key[]>>] :
[]
// prettier-ignore
export type TIndexReduce<T extends TSchema, K extends Key[]> =
T extends TRecursive<infer S> ? TIndexReduce<S, K> :
export type TIndex<T extends TSchema, K extends Key[]> =
T extends TRecursive<infer S> ? TIndex<S, K> :
T extends TIntersect ? UnionType<Flat<TIndexRestMany<T, K>>> :
T extends TUnion ? UnionType<Flat<TIndexRestMany<T, K>>> :
T extends TObject ? UnionType<Flat<TIndexRestMany<T, K>>> :
T extends TTuple ? UnionType<Flat<TIndexRestMany<T, K>>> :
TNever
// prettier-ignore
export type TIndex<T extends TSchema, K extends TSchema> =
[T, K] extends [TTuple, TNumber] ? UnionType<Assert<T['items'], TSchema[]>> :
[T, K] extends [TArray, TNumber] ? AssertType<T['items']> :
K extends TTemplateLiteral ? TIndexReduce<T, TTemplateLiteralKeyRest<K>> :
K extends TUnion<TLiteral<Key>[]> ? TIndexReduce<T, TUnionLiteralKeyRest<K>> :
K extends TLiteral<Key> ? TIndexReduce<T, [K['const']]> :
TNever
// --------------------------------------------------------------------------
// TInteger
// --------------------------------------------------------------------------
@@ -393,10 +385,10 @@ export interface TNever extends TSchema {
// --------------------------------------------------------------------------
// TNot
// --------------------------------------------------------------------------
export interface TNot<Not extends TSchema = TSchema, T extends TSchema = TSchema> extends TSchema {
export interface TNot<T extends TSchema = TSchema> extends TSchema {
[Kind]: 'Not'
static: Static<T>
allOf: [{ not: Not }, T]
static: T extends TNot<infer U> ? Static<U> : 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<T extends TNot>(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<T extends TSchema, K extends (keyof Static<T>)[]>(schema: T, keys: [...K], options?: SchemaOptions): TIndexReduce<T, Assert<K, Key[]>>
public Index<T extends TTuple, K extends TNumber>(schema: T, keys: K, options?: SchemaOptions): UnionType<Assert<T['items'], TSchema[]>>
/** `[Standard]` Returns indexed property types for the given keys */
public Index<T extends TSchema, K extends TSchema>(schema: T, key: K, options?: SchemaOptions): TIndex<T, K>
public Index<T extends TArray, K extends TNumber>(schema: T, keys: K, options?: SchemaOptions): AssertType<T['items']>
/** `[Standard]` Returns indexed property types for the given keys */
public Index<T extends TSchema, K extends TTemplateLiteral>(schema: T, keys: K, options?: SchemaOptions): TIndex<T, TTemplateLiteralKeyRest<K>>
/** `[Standard]` Returns indexed property types for the given keys */
public Index<T extends TSchema, K extends TLiteral<Key>>(schema: T, keys: K, options?: SchemaOptions): TIndex<T, [K['const']]>
/** `[Standard]` Returns indexed property types for the given keys */
public Index<T extends TSchema, K extends (keyof Static<T>)[]>(schema: T, keys: [...K], options?: SchemaOptions): TIndex<T, Assert<K, Key[]>>
/** `[Standard]` Returns indexed property types for the given keys */
public Index<T extends TSchema, K extends TUnion<TLiteral<Key>[]>>(schema: T, keys: K, options?: SchemaOptions): TIndex<T, TUnionLiteralKeyRest<K>>
/** `[Standard]` Returns indexed property types for the given keys */
public Index<T extends TSchema, K extends TSchema>(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<N extends TSchema, T extends TSchema>(not: N, schema: T, options?: SchemaOptions): TNot<N, T> {
return this.Create({ ...options, [Kind]: 'Not', allOf: [{ not: TypeClone.Clone(not, {}) }, TypeClone.Clone(schema, {})] })
/** `[Standard]` Creates a Not type */
public Not<T extends TSchema>(not: T, options?: SchemaOptions): TNot<T> {
return this.Create({ ...options, [Kind]: 'Not', not })
}
/** `[Standard]` Creates a Null type */
public Null(options: SchemaOptions = {}): TNull {

View File

@@ -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)

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 })
})
})

View File

@@ -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 })

View File

@@ -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 })
})
})

View File

@@ -7,6 +7,7 @@ import './date'
import './function'
import './integer'
import './literal'
import './not'
import './null'
import './number'
import './object'

View File

@@ -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<any> ? 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<string> ? 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)
})
})

View File

@@ -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)
})
})

View File

@@ -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)
})
})

View File

@@ -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)
})
})

View File

@@ -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)
})

View File

@@ -2,6 +2,10 @@ import { Expect } from './assert'
import { Type } from '@sinclair/typebox'
{
const T = Type.Not(Type.Number(), Type.String())
Expect(T).ToInfer<string>()
const T = Type.Not(Type.Number())
Expect(T).ToInfer<unknown>()
}
{
const T = Type.Not(Type.Not(Type.Number()))
Expect(T).ToInfer<number>()
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 924 KiB

After

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB