Skip to content

Latest commit

 

History

History
644 lines (435 loc) · 17.8 KB

File metadata and controls

644 lines (435 loc) · 17.8 KB

1.0.0 Migration Guide

Overview

This document covers upgrading from TypeBox 0.34.x to 1.0. While most of the API surface remains intact for 1.0, there are several key breaking changes that will impact users who integrate deeply with TypeBox infrastructure. The sections below document these changes and detail strategies for upgrading.

Contents

ESM Package

Version 1.0 is published as an ESM-only package. Users will need to ensure their environments support ESM module resolution.

Kind, Hint, ReadonlyKind and OptionalKind

Version 1.0 removes the Kind, ReadonlyKind, OptionalKind symbols. These symbols were originally introduced to enable fast type differentiation internal to the library, as well as to solve integration issues with Ajv strict mode.

The use of symbols have proven difficult for users who install multiple versions of TypeBox side by side. This is most notable in frameworks that install one version of TypeBox and where users install another. The removal of symbols will enable multiple version of TypeBox to run side by side without requiring users to pin specific versions via package.json.

Version 1.0 replaces each symbol with non-enumerable ~kind, ~readonly and ~optional properties. Applications that use symbols to reflect TypeBox types will need to update to read these properties instead.

Example

0.34.x

import { Type, Kind } from '@sinclair/typebox'

const T = Type.String()

if(T[Kind] === 'String') { ... }

1.0.0

import Type from 'typebox'

const T = Type.String()

if(T['~kind'] === 'String') { ... }

// Consider using guards instead of direct property access.

if(Type.IsString(T)) { ... }

Type.Date and Type.Uint8Array

Version 1.0 removes the Date and Uint8Array types. These types can be created at an application level by extending Type.Base.

Fallback

Uint8Array | Date

Example

The following implements Uint8Array using Type.Base.

import Type from 'typebox'

export class TUint8Array extends Type.Base<Uint8Array> {
  // required: Used by validation
  public override Check(value: unknown): value is Uint8Array {
    return value instanceof Uint8Array
  }
  // required: Used by validation
  public override Errors(value: unknown): object[] {
    return !this.Check(value) ? [{ message: 'not a Uint8Array'}] : []
  }
  // required: Used by type compositor
  public override Clone(): TUint8Array {
    return new TUint8Array()
  }
}
export function Uint8Array(): TUint8Array {
  return new TUint8Array()
}

Type.Recursive

Version 1.0 removes the Recursive type. This type has been replaced with a new Cyclic type which supports both singular and mutual recursive types.

Fallback

Recursive

Example

0.34.x

const Node = Type.Recursive(This => Type.Object({
  id: Type.Number(),
  nodes: Type.Array(This)
}))

1.0.0

const Node = Type.Cyclic({
  Node: Type.Object({
    id: Type.Number(),
    nodes: Type.Array(Type.Ref('Node'))
  })
}, 'Node')

Type.RegExp

Version 1.0 removes the RegExp type. TypeBox now allows regular expressions to be passed via pattern on String types.

Fallback

RegExp

Example

0.34.x

const T = Type.RegExp(/abc/i)

1.0.0

const T = Type.String({ pattern: /abc/i })

Type.Composite

Version 1.0 removes the Composite type. This type has been replaced by the Interface and Evaluate types.

Fallback

Composite

Example

0.34.x

const A = Type.Object({ a: Type.Number() })
const B = Type.Object({ b: Type.Number() })
const C = Type.Composite([A, B])

1.0.0

// Composite with Interface and Heritage
const A = Type.Object({ a: Type.Number() })
const B = Type.Object({ b: Type.Number() })
const C = Type.Interface([A, B], { c: Type.Number() }) // const C: TObject<{
                                                       //   a: TNumber,
                                                       //   b: TNumber,
                                                       //   c: TNumber
                                                       // }> 

// Composite with Evaluate
const A = Type.Object({ a: Type.Number() })
const B = Type.Object({ b: Type.Number() })
const C = Type.Object({ c: Type.Number() })
const T = Type.Evaluate(Type.Intersect([A, B, C]))     // const T: TObject<{
                                                       //   a: TNumber,
                                                       //   b: TNumber,
                                                       //   c: TNumber
                                                       // }> 

Type.Transform

The Transform type has been renamed to Codec. This change better reflects the distinction between bi-directional vs uni-directional transformation; where 1.0 adds support for uni-directional transforms.

Fallback

Transform

Example

0.34.x

const NumberToString = Type.Transform(Type.Number())
  .Decode(value => value.toString())
  .Encode(value => parseFloat(value))

1.0.0

// Bi-Directional
const NumberToString = Type.Codec(Type.Number())
  .Decode(value => value.toString())
  .Encode(value => parseFloat(value))

// Uni-Directional
const NumberToStringDecode = Type.Decode(Type.Number(), value => value.toString())
const NumberToStringEncode = Type.Encode(Type.Number(), (value: number) => value.toString())

Type.Const

The Const type has been removed in 1.0. This type was originally used to create structural type representations from constant TypeScript values. 1.0 deprecates this functionality in favor of Script which can parse values into structural representations.

Fallback

Const

Example

0.34.x

const T = Type.Const({ x: 1, y: 2, z: 3 } as const) // const T: TObject<{
                                                    //   TReadonly<TLiteral<1>>
                                                    //   TReadonly<TLiteral<2>>
                                                    //   TReadonly<TLiteral<3>>
                                                    // }>

1.0.0

const T = Type.Script(`{ x: 1, y: 2, z: 3 }`)       // const T: TObject<{
                                                    //   x: TLiteral<1>;
                                                    //   y: TLiteral<2>;
                                                    //   z: TLiteral<3>;
                                                    // }>

// Optional: If readonly is required.

const S = Type.Script({ T }, `{
  readonly [K in keyof T]: T[K]  
}`)                                                 // const S: TObject<{
                                                    //   x: TReadonly<TLiteral<1>>;
                                                    //   y: TReadonly<TLiteral<2>>;
                                                    //   z: TReadonly<TLiteral<3>>;
                                                    // }>

Type.Pick and Type.Omit

Version 1.0 updates Pick and Omit to return evaluated Object types when applied to Union and Intersect types. This change aligns with V1’s ability to evaluate logical type expressions for mapped types. As a consequence, the return structures of Pick and Omit may vary depending on the source type being used.

In 0.34.x, Pick and Omit would simply traverse a logical type expression and select properties from embedded Object types. In 1.0, Pick and Omit evaluate the result into a normalized Object type. Both 0.34.x and 1.0 return semantically equivalent types, but in 1.0 the result is reduced to its simplest form.

Fallback

Pick | Omit

Example

0.34.x

const X = Type.Object({ x: Type.Number(), z: Type.Number() })
const Y = Type.Object({ y: Type.Number(), z: Type.Literal(1) })
const Z = Type.Intersect([X, Y])

const T = Type.Pick(Z, ['z'])                       // const T: TIntersect<[TObject<{
                                                    //   z: TNumber;
                                                    // }>, TObject<{
                                                    //   z: TLiteral<1>
                                                    // }>]>

1.0.0

const X = Type.Object({ x: Type.Number(), z: Type.Number() })
const Y = Type.Object({ y: Type.Number(), z: Type.Literal(1) })
const Z = Type.Intersect([X, Y])

const T = Type.Pick(Z, ['z'])                       // const T: Type.TObject<{
                                                    //   z: Type.TLiteral<1>;
                                                    // }>

TypeCompiler

Version 1.0 makes API changes to the TypeBox compiler but retains the same functionality. The biggest change in 1.0 is the renaming of TypeCheck to Validator. Implementations that reference TypeCheck will need to updated reference Validator. Additionally, the TypeCompiler namespace has been deprecated, implementations can now use the Compile function exported on the submodule.

Example

0.34.x

import { TypeCompiler, TypeCheck } from '@sinclair/typebox/compiler'

const C = TypeCompiler.Compile(Type.String())       // TypeCheck<TString>

C.Check('hello') // true

1.0.0

import { Compile, Validator } from 'typebox/compile'

const C = Compile(Type.String())                    // Validator<{}, TString>

C.Check('hello') // true

References

Version 1.0 introduces a model for handling referential types. This model is reflected across Value and Compiler submodules. In 1.0, references are new passed via a optional Context object. This replaces the references array in 0.34.x.

Example

0.34.x

const A = Type.String({ $id: 'A' })
const B = Type.Number({ $id: 'B' })

const T = Type.Object({
  a: Type.Unsafe<Static<typeof A>>(Type.Ref('A')),  // Unsafe Static for Inference
  b: Type.Unsafe<Static<typeof B>>(Type.Ref('B')),
})

const C = TypeCompiler.Compile(T, [A, B])           // References Array

const R = C.Decode({ a: '', b: 1 })                 // const R: { a: string, b: number }

1.0.0

const A = Type.String()
const B = Type.Number()

const T = Type.Object({
  a: Type.Ref('A'),
  b: Type.Ref('B')
})

const C = Compile({ A, B }, T)                      // const R: { a: string, b: number }

FormatRegistry

TypeBox 1.0 moves the FormatRegistry to the Format submodule.

Example

0.34.x

import { FormatRegistry } from '@sinclair/typebox'

FormatRegistry.Set('foo', value => value === 'foo')

1.0.0

import Format from 'typebox/format'

Format.Set('foo', value => value === 'foo')

TypeRegistry

TypeBox 1.0 removes the TypeRegistry. This registry has been replaced by Type.Base that enables custom types to be created without the need for registration. The Base type provides the same capabilities as the TypeRegistry but is structured quite differently. The example comments show the changes.

Example

0.34.x

import { TypeRegistry, Kind, TSchema } from '@sinclair/typebox'

// Definition
interface TFoo extends TSchema {
  [Kind]: 'Foo'  
  static: 'Foo'  // Static
}
// Check
TypeRegistry.Set('Foo', (value) => value === 'Foo')

// Factory
function Foo(): TFoo {
  return { [Kind]: 'Foo' } as never
}

1.0.0

import Type from '@sinclair/typebox'

// Definition
export class TFoo extends Type.Base<'Foo'> { // Static
  // Check
  public override Check(value: unknown): value is 'Foo' {
    return value === 'Foo'
  }
}
// Factory
export function Foo(): TDate {
  return new TDate()
}

TypeGuard

TypeBox 1.0 removes the TypeGuard. The Is functions have been moved to Type.*.

Example

0.34.x

import { Type, TypeGuard } from '@sinclair/typebox'

const T = Type.String()

const R = TypeGuard.IsString(T)                     // const R = true

1.0.0

import Type from 'typebox'

const T = Type.String()

const R = Type.IsString(T)                          // const R = true

Value.Errors

Version 1.0 changes the Errors() return type from Iterator to Array. It also updates the Error to be inline with Ajv. This change enables TypeBox errors to integrate with existing systems that report errors using the Ajv structure. Existing applications will need to update error handling inline with the new structure.

Error Interface

0.34.x

export interface ValueError {
  type: ValueErrorType
  schema: TSchema
  path: string
  value: unknown
  message: string
  errors: ValueErrorIterator[]
}

1.0.0

interface TLocalizedValidationError {
  keyword: string
  schemaPath: string
  instancePath: string
  params: object
  message: string
}

Example

0.34.x

import { Value } from '@sinclair/typebox/value'

const T = Type.Number()

const E = [...Value.Errors(T, 'not a number')]      // IterableIterator<Error>

1.0.0

import Value from 'typebox/value'

const T = Type.Number()

const E = Value.Errors(T, 'not a number')            // TLocalizedValidationError[]

Value.Cast

Version 1.0 renames Cast to Repair.

Example

0.34.x

const T = Type.Object({ x: Type.Number() })

const R = Value.Cast(T, { })                        // const R = { x: 0 }

1.0.0

const T = Type.Object({ x: Type.Number() })

const R = Value.Repair(T, { })                        // const R = { x: 0 }

SetErrorFunction

TypeBox 1.0 removes the SetErrorFunction callback. This callback was original written to provide internationalization (i18n) hooks for error messages. As Version 1.0 adds built-in support for error message localization, the SetErrorFunction as been removed. Error localization has been moved to the system module.

Example

0.34.x

import { SetErrorFunction, DefaultErrorFunction, ValueErrorType } from '@sinclair/typebox/errors'

SetErrorFunction((error) => { // i18n override
  switch(error.errorType) {
    /* en-US */ case ValueErrorType.String: return 'Expected string'
    /* fr-FR */ case ValueErrorType.Number: return 'Nombre attendu'  
    /* ko-KR */ case ValueErrorType.Boolean: return '예상 부울'      
    /* en-US */ default: return DefaultErrorFunction(error)          
  }
})

1.0.0

import { Locale } from 'typebox/system'

Locale.Set(Locale.en_US)                            // en-US
Locale.Set(Locale.fr_FR)                            // fr-FR
Locale.Set(Locale.ko_KR)                            // ko-KR
Locale.Reset()                                      // en-US

CustomErrors

A common pattern for handling custom errors in 0.34.x was to use the SetErrorFunction to intercept the errorMessage property and remap errors when it was present. Since SetErrorFunction has been removed in 1.0, implementations will need to adopt a new strategy for overriding error messages. In 1.0, errors are structured inline with Ajv errors to promote better interoperability with reporting tools that understand Ajv structures, and the mechanisms for handling sub-schema dereferencing have changed. Refer to the provided custom-errors.ts fallback as a reference for handling custom error messages in 1.0.

Fallback

CustomErrors

0.34.x

SetErrorFunction((parameter) =>
  "errorMessage" in parameter.schema
    ? (parameter.schema.errorMessage as string)
    : DefaultErrorFunction(parameter),
)

1.0.0

import { CustomErrors } from './legacy/custom-errors' // refer to fallback implementation
import Type from 'typebox'

const T = Type.Object({
  x: Type.Number({ errorMessage: 'Expected X Component' }),
  y: Type.Number({ errorMessage: 'Expected Y Component' })
})

const E = CustomErrors(T, { x: '0', y: '0' })       // const E = [{
                                                    //   keyword: 'type',
                                                    //   schemaPath: '#/properties/x',  
                                                    //   instancePath: '/x',
                                                    //   params: { type: "number" },    
                                                    //   message: "Expected X Component"
                                                    // }, {
                                                    //   keyword: "type",
                                                    //   schemaPath: '#/properties/y',  
                                                    //   instancePath: '/y',
                                                    //   params: { type: "number" },    
                                                    //   message: 'Expected Y Component'
                                                    // }]