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.1 #485

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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