deno.land / x / typebox@0.32.21 / changelog / 0.28.0.md
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.
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'>
])) // ]>
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,
// ]>
The following are breaking changes in Revision 0.28.0
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
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.
Version Info