Skip to content

Commit

Permalink
Revision 0.29.1 (#485)
Browse files Browse the repository at this point in the history
  • Loading branch information
sinclairzx81 authored Jul 2, 2023
1 parent 337532d commit bdeb24f
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 11 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sinclair/typebox",
"version": "0.29.0",
"version": "0.29.1",
"description": "JSONSchema Type Builder with Static Type Resolution for TypeScript",
"keywords": [
"typescript",
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,7 @@ The following is a list of community packages that provide general tooling and f
| [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 |
| [typebox-client](https://github.com/flodlc/typebox-client) | Type safe http client library for Fastify |
| [typebox-validators](https://github.com/jtlapp/typebox-validators) | Advanced validators supporting discriminated and heterogeneous unions |
<a name='benchmark'></a>
Expand Down
32 changes: 24 additions & 8 deletions src/typebox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1540,7 +1540,7 @@ export namespace TypeExtends {
// --------------------------------------------------------------------------
// Not
// --------------------------------------------------------------------------
function ResolveNot<T extends TNot>(schema: T): TUnknown | TNot['not'] {
function UnwrapNot<T extends TNot>(schema: T): TUnknown | TNot['not'] {
let [current, depth]: [TSchema, number] = [schema, 0]
while (true) {
if (!TypeGuard.TNot(current)) break
Expand All @@ -1549,6 +1549,14 @@ export namespace TypeExtends {
}
return depth % 2 === 0 ? current : Type.Unknown()
}
function Not(left: TSchema, right: TSchema) {
// TypeScript has no concept of negated types, and attempts to correctly check the negated
// type at runtime would put TypeBox at odds with TypeScripts ability to statically infer
// the type. Instead we unwrap to either unknown or T and continue evaluating.
if (TypeGuard.TNot(left)) return Visit(UnwrapNot(left), right)
if (TypeGuard.TNot(right)) return Visit(left, UnwrapNot(right))
throw new Error(`TypeExtends: Invalid fallthrough for Not`)
}
// --------------------------------------------------------------------------
// Null
// --------------------------------------------------------------------------
Expand Down Expand Up @@ -1764,6 +1772,17 @@ export namespace TypeExtends {
return TypeGuard.TSymbol(right) ? TypeExtendsResult.True : TypeExtendsResult.False
}
// --------------------------------------------------------------------------
// TemplateLiteral
// --------------------------------------------------------------------------
function TemplateLiteral(left: TSchema, right: TSchema) {
// TemplateLiteral types are resolved to either unions for finite expressions or string
// for infinite expressions. Here we call to TemplateLiteralResolver to resolve for
// either type and continue evaluating.
if (TypeGuard.TTemplateLiteral(left)) return Visit(TemplateLiteralResolver.Resolve(left), right)
if (TypeGuard.TTemplateLiteral(right)) return Visit(left, TemplateLiteralResolver.Resolve(right))
throw new Error(`TypeExtends: Invalid fallthrough for TemplateLiteral`)
}
// --------------------------------------------------------------------------
// Tuple
// --------------------------------------------------------------------------
function TupleRight(left: TSchema, right: TTuple) {
Expand Down Expand Up @@ -1857,13 +1876,10 @@ 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))
// Standard Extends
// Resolvable Types
if (TypeGuard.TTemplateLiteral(left) || TypeGuard.TTemplateLiteral(right)) return TemplateLiteral(left, right)
if (TypeGuard.TNot(left) || TypeGuard.TNot(right)) return Not(left, right)
// Standard Types
if (TypeGuard.TAny(left)) return Any(left, right)
if (TypeGuard.TArray(left)) return Array(left, right)
if (TypeGuard.TBigInt(left)) return BigInt(left, right)
Expand Down
15 changes: 15 additions & 0 deletions test/runtime/type/extends/not.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ import { Assert } from '../../assert/index'
// Note: Not is equivalent to Unknown with the exception of nested negation.
// ---------------------------------------------------------------------------
describe('type/extends/Not', () => {
// -------------------------------------------------------------------------
// Issue: type T = number extends not number ? true : false // true
// type T = number extends unknown ? true : false // true
//
// TypeScript does not support type negation. The best TypeBox can do is
// treat "not" as "unknown". From this standpoint, the extends assignability
// check needs to return true for the following case to keep TypeBox aligned
// with TypeScript static inference.
// -------------------------------------------------------------------------
it('Should extend with unknown assignability check', () => {
const A = Type.Number()
const B = Type.Not(Type.Number())
const R = TypeExtends.Extends(A, B)
Assert.isEqual(R, TypeExtendsResult.True) // we would expect false
})
// ---------------------------------------------------------------------------
// Nested
// ---------------------------------------------------------------------------
Expand Down
15 changes: 15 additions & 0 deletions test/static/not.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { Expect } from './assert'
import { Type } from '@sinclair/typebox'

{
// -------------------------------------------------------------------------
// Issue: type T = number extends not number ? true : false // true
// type T = number extends unknown ? true : false // true
//
// TypeScript does not support type negation. The best TypeBox can do is
// treat "not" as "unknown". From this standpoint, the extends assignability
// check needs to return true for the following case to keep TypeBox aligned
// with TypeScript static inference.
// -------------------------------------------------------------------------
const A = Type.Number()
const B = Type.Not(Type.Number())
const T = Type.Extends(A, B, Type.Literal(true), Type.Literal(false))
Expect(T).ToInfer<true>()
}
{
const T = Type.Not(Type.Number())
Expect(T).ToInfer<unknown>()
Expand Down

0 comments on commit bdeb24f

Please sign in to comment.