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

Revision 0.29.0 #483

merged 2 commits into from Jul 2, 2023

Conversation

sinclairzx81
Copy link
Owner

@sinclairzx81 sinclairzx81 commented Jul 1, 2023

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.

Contents

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

// 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.

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

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

// 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 intersection type narrowing capabilities of TypeScript.

Not Inversion

The not type can be inverted through nesting.

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

Inference Limitations

Not types are synonymous with the concept of negated types which are not supported in the TypeScript language. Because of this, it is not currently possible to infer negated types in a way one would naturally expect for some cases. Consider the following.

const T = Type.Intersect([Type.String(), Type.Not(Type.String())])

type T = Static<typeof T> // type T = string & not string
                          // actual: string
                          // expect: never

As such, the use of Not types should be used with some consideration to current limitations, and reserved primarily for narrowing cases such as the following.

const T = Type.Intersect([Type.String(), Type.Not(Type.Literal('disallowed string'))])

type T = Static<typeof T> // type T = string & not 'disallowed string'
                          // actual: string
                          // expect: string

@sinclairzx81 sinclairzx81 merged commit ebef7cc into master Jul 2, 2023
18 checks passed
@sinclairzx81 sinclairzx81 deleted the not branch July 2, 2023 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant