## [0.28.0](https://www.npmjs.com/package/@sinclair/typebox/v/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

- Enhancements
  - [Indexed Access Types](#Indexed-Access-Types)
  - [KeyOf Tuple and Array](#KeyOf-Tuple-and-Array)
- Breaking Changes
  - [Record Types Allow Additional Properties By Default](#Record-Types-Allow-Additional-Properties-By-Default)
  - [Composite Returns Intersect for Overlapping Properties](#Composite-Returns-Intersect-for-Overlapping-Properties)

<a href="Indexed-Access-Types"></a>

## Indexed Access Types

Revision 0.28.0 adds [Indexed Access Type](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) 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.

```typescript
// ----------------------------------------------------------
// 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.
```typescript
// -----------------------------------------------------------
// 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'>
]))                                       // ]>
```

<a href="KeyOf-Tuple-and-Array"></a>

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

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

<a href="Record-Types-Allow-Additional-Properties-By-Default"></a>

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

```typescript
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.

```typescript
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
```

<a href="Composite-Returns-Intersect-for-Overlapping-Properties"></a>

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

```typescript
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. 