Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revision 0.29.0 #483

Merged
merged 2 commits into from
Jul 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions changelog/0.29.0.md
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -42,6 +42,6 @@
"chai": "^4.3.6",
"mocha": "^9.2.2",
"prettier": "^2.7.1",
"typescript": "^5.1.3"
"typescript": "^5.1.6"
}
}
77 changes: 54 additions & 23 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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, │
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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>

Expand Down
5 changes: 2 additions & 3 deletions src/compiler/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down
3 changes: 1 addition & 2 deletions src/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Loading