deno.land / x / typebox@0.32.21 / changelog / 0.28.0.md

0.28.0

Overview

Revision 0.28.0 adds support for Indexed Access Types. This update also includes moderate breaking changes to Record and Composite types and does require a minor semver revision tick.

Contents

Indexed Access Types

Revision 0.28.0 adds Indexed Access Type support with a new Type.Index() mapping type. These types allow for deep property lookups without needing to prop dive through JSON Schema properties. This type is based on the TypeScript implementation of Indexed Access Types and allows for generalized selection of properties for complex types irrespective of if that type is a Object, Union, Intersection, Array or Tuple.

// ----------------------------------------------------------
// The following types A and B are structurally equivalent, 
// but have varying JSON Schema representations.
// ----------------------------------------------------------
const A = Type.Object({
  x: Type.Number(),
  y: Type.String(),
  z: Type.Boolean(),
})

const B = Type.Intersect([           
  Type.Object({ x: Type.Number() }),
  Type.Object({ y: Type.String() }),
  Type.Object({ z: Type.Boolean() })
])

// ----------------------------------------------------------
// TypeBox 0.27.0 - Non Uniform
// ----------------------------------------------------------
const A_X = A.properties.x                // TNumber
const A_Y = A.properties.y                // TString
const A_Z = A.properties.z                // TBoolean

const B_X = B.allOf[0].properties.x       // TNumber
const B_Y = B.allOf[1].properties.y       // TString
const B_Z = B.allOf[2].properties.z       // TBoolean

// ----------------------------------------------------------
// TypeBox 0.28.0 - Uniform via Type.Index
// ----------------------------------------------------------
const A_X = Type.Index(A, ['x'])          // TNumber
const A_Y = Type.Index(A, ['y'])          // TString
const A_Z = Type.Index(A, ['z'])          // TBoolean

const B_X = Type.Index(B, ['x'])          // TNumber
const B_Y = Type.Index(B, ['y'])          // TString
const B_Z = Type.Index(B, ['z'])          // TBoolean

Indexed Access Types support has also been extended to Tuple and Array types.

// -----------------------------------------------------------
// Array
// -----------------------------------------------------------
type T = string[]

type I = T[number]                        // type T = string

const T = Type.Array(Type.String())

const I = Type.Index(T, Type.Number())    // const I = TString

// -----------------------------------------------------------
// Tuple
// -----------------------------------------------------------
type T = ['A', 'B', 'C']

type I = T[0 | 1]                         // type I = 'A' | 'B'

const T = Type.Array(Type.String())

const I = Type.Index(T, Type.Union([      // const I = TUnion<[
  Type.Literal(0),                        //   TLiteral<'A'>,
  Type.Literal(1),                        //   TLiteral<'B'>
]))                                       // ]>

KeyOf Tuple and Array

Revision 0.28.0 includes additional Type.KeyOf support for Array and Tuple types. Keys of Array will always return TNumber, whereas keys of Tuple will return a LiteralUnion for each index of that tuple.

// -----------------------------------------------------------
// KeyOf: Tuple
// -----------------------------------------------------------
const T = Type.Tuple([Type.Number(), Type.Number(), Type.Number()])

const K = Type.KeyOf(T)                   // const K = TUnion<[
                                          //   TLiteral<'0'>,
                                          //   TLiteral<'1'>,
                                          //   TLiteral<'2'>,
                                          // ]>

// -----------------------------------------------------------
// KeyOf: Array
// -----------------------------------------------------------
const T = Type.Array(Type.String())

const K = Type.KeyOf(T)                   // const K = TNumber

It is possible to combine KeyOf with Index types to extract properties from array and object constructs.

// -----------------------------------------------------------
// KeyOf + Index: Object
// -----------------------------------------------------------
const T = Type.Object({ x: Type.Number(),  y: Type.String(),  z: Type.Boolean() })

const K = Type.Index(T, Type.KeyOf(T))    // const K = TUnion<[
                                          //   TNumber,
                                          //   TString,
                                          //   TBoolean,
                                          // ]>   

// -----------------------------------------------------------
// KeyOf + Index: Tuple
// -----------------------------------------------------------
const T = Type.Tuple([Type.Number(), Type.String(), Type.Boolean()])

const K = Type.Index(T, Type.KeyOf(T))    // const K = TUnion<[
                                          //   TNumber,
                                          //   TString,
                                          //   TBoolean,
                                          // ]>               

Breaking Changes

The following are breaking changes in Revision 0.28.0

Record Types Allow Additional Properties By Default

Revision 0.28.0 no longer applies an automatic additionalProperties: false constraint to types of TRecord. Previously this constraint was set to prevent records with numeric keys from allowing unevaluated additional properties with non-numeric keys. This constraint worked in revisions up to 0.26.0, but since the move to use allOf intersect schema representations, this meant that types of Record could no longer be composed with intersections. This is due to the JSON Schema rules around extending closed schemas. Information on these rules can be found at the link below.

https://json-schema.org/understanding-json-schema/reference/object.html#extending-closed-schemas

For the most part, the omission of this constraint shouldn't impact existing record types with string keys, however numeric keys may cause problems. Consider the following where the validation unexpectedly succeeds for the following numeric keyed record.

const T = Type.Record(Type.Number(), Type.String())

const R = Value.Check(T, { a: null }) // true - Because `a` is non-numeric and thus is treated as an
                                      //        additional unevaluated property.

Moving forward, Records with numeric keys "should" be constrained explicitly with additionalProperties: false via options if that record does not require composition through intersection. This is largely inline with the existing constraints one might apply to types of Object.

const T = Type.Record(Type.Number(), Type.String(), {
  additionalProperties: false
})

const R = Value.Check(T, { a: null }) // false - Because `a` is non-numeric additional property

Composite Returns Intersect for Overlapping Properties

This is a minor breaking change with respect to the schema returned for Composite objects with overlapping varying property types. Previously TypeBox would evaluate TNever by performing an internal extends check against each overlapping property type. However problems emerged using this implementation for users who needed to use Composite with types of TUnsafe. This is due to unsafe types being incompatible with TypeBox's internal extends logic.

The solution implemented in 0.28.0 is to return the full intersection of all overlapping properties. The reasoning here is that if the overlapping properties of varying types result in an illogical intersection, this is semantically the same as resolving never for that property. This approach avoids the need to internally check if all overlapping properties extend or narrow one another.

const T = Type.Composite([
  Type.Object({ x: Type.Number() }),  // overlapping property 'x' of varying type
  Type.Object({ x: Type.String() })
])

// -----------------------------------------------------------
// Revision 0.27.0
// -----------------------------------------------------------
const R = Type.Object({
  x: Type.Never()                    // Never evaluated through extends checks.
})

// -----------------------------------------------------------
// Revision 0.28.0
// -----------------------------------------------------------
const R = Type.Object({
  x: Type.Intersect([               // Illogical intersections are semantically the same as never
    Type.Number(), 
    Type.String()
  ])
})

This implementation should make it more clear what the internal mechanics are for object compositing. Future revisions of TypeBox may however provide a utility function to test illogical intersections for Never for known types.

typebox

Version Info

Tagged at
4 weeks ago