General Maintenance (#489)

This commit is contained in:
sinclairzx81
2023-07-05 05:05:16 +09:00
committed by GitHub
parent 3f50917d98
commit 185eb13dc9
14 changed files with 546 additions and 853 deletions

View File

@@ -0,0 +1,3 @@
# Collections
This example implements runtime type safe generic `Array`, `Map` and `Set` collection types using TypeBox types as the generic type arguments.

View File

@@ -1,109 +0,0 @@
/*--------------------------------------------------------------------------
@sinclair/typebox/experimental
The MIT License (MIT)
Copyright (c) 2017-2023 Haydn Paterson (sinclair) <haydn.developer@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---------------------------------------------------------------------------*/
import * as Types from '@sinclair/typebox'
import { Value } from '@sinclair/typebox/value'
// -------------------------------------------------------------------------------------
// TReadonlyObject
// -------------------------------------------------------------------------------------
export type TReadonlyArray<T extends Types.TSchema[]> = Types.Assert<{ [K in keyof T]: TReadonlyObject<Types.Assert<T[K], Types.TSchema>> }, Types.TSchema[]>
// prettier-ignore
export type TReadonlyProperties<T extends Types.TProperties> = Types.Evaluate<Types.Assert<{
[K in keyof T]:
T[K] extends Types.TReadonlyOptional<infer U> ? Types.TReadonlyOptional<U> :
T[K] extends Types.TReadonly<infer U> ? Types.TReadonly<U> :
T[K] extends Types.TOptional<infer U> ? Types.TReadonlyOptional<U> :
Types.TReadonly<T[K]>
}, Types.TProperties>>
// prettier-ignore
export type TReadonlyObject<T extends Types.TSchema> =
T extends Types.TIntersect<infer S> ? Types.TIntersect<TReadonlyArray<S>> :
T extends Types.TUnion<infer S> ? Types.TUnion<TReadonlyArray<S>> :
T extends Types.TObject<infer S> ? Types.TObject<TReadonlyProperties<S>> :
Types.TReadonly<T>
// -------------------------------------------------------------------------------------
// TUnionEnum
// -------------------------------------------------------------------------------------
export interface TUnionEnum<T extends (string | number)[]> extends Types.TSchema {
[Types.Kind]: 'UnionEnum'
static: T[number]
enum: T
}
// -------------------------------------------------------------------------------------
// UnionOneOf
// -------------------------------------------------------------------------------------
export interface UnionOneOf<T extends Types.TSchema[]> extends Types.TSchema {
[Types.Kind]: 'UnionOneOf'
static: { [K in keyof T]: Types.Static<T[K]> }[number]
oneOf: T
}
// -------------------------------------------------------------------------------------
// ExperimentalTypeBuilder
// -------------------------------------------------------------------------------------
export class ExperimentalTypeBuilder extends Types.ExtendedTypeBuilder {
/** `[Experimental]` Remaps a Intersect, Union or Object as readonly */
public ReadonlyObject<T extends Types.TSchema>(schema: T): TReadonlyObject<T> {
function Apply(property: Types.TSchema): any {
// prettier-ignore
switch (property[Types.Modifier]) {
case 'ReadonlyOptional': property[Types.Modifier] = 'ReadonlyOptional'; break
case 'Readonly': property[Types.Modifier] = 'Readonly'; break
case 'Optional': property[Types.Modifier] = 'ReadonlyOptional'; break
default: property[Types.Modifier] = 'Readonly'; break
}
return property
}
// prettier-ignore
return (Types.TypeGuard.TIntersect(schema) || Types.TypeGuard.TUnion(schema) || Types.TypeGuard.TObject(schema))
? Types.ObjectMap.Map<TReadonlyObject<T>>(schema, (schema) => {
globalThis.Object.keys(schema.properties).forEach(key => Apply(schema.properties[key]))
return schema
}, {}) : Apply(schema)
}
/** `[Experimental]` Creates a Union type with a `enum` schema representation */
public UnionEnum<T extends (string | number)[]>(values: [...T], options: Types.SchemaOptions = {}) {
function UnionEnumCheck(schema: TUnionEnum<(string | number)[]>, value: unknown) {
return (typeof value === 'string' || typeof value === 'number') && schema.enum.includes(value)
}
if (!Types.TypeRegistry.Has('UnionEnum')) Types.TypeRegistry.Set('UnionEnum', UnionEnumCheck)
return { ...options, [Types.Kind]: 'UnionEnum', enum: values } as TUnionEnum<T>
}
/** `[Experimental]` Creates a Union type with a `oneOf` schema representation */
public UnionOneOf<T extends Types.TSchema[]>(oneOf: [...T], options: Types.SchemaOptions = {}) {
function UnionOneOfCheck(schema: UnionOneOf<Types.TSchema[]>, value: unknown) {
return 1 === schema.oneOf.reduce((acc: number, schema: any) => (Value.Check(schema, value) ? acc + 1 : acc), 0)
}
if (!Types.TypeRegistry.Has('UnionOneOf')) Types.TypeRegistry.Set('UnionOneOf', UnionOneOfCheck)
return { ...options, [Types.Kind]: 'UnionOneOf', oneOf } as UnionOneOf<T>
}
}
export const Type = new ExperimentalTypeBuilder()

View File

@@ -1,6 +1,6 @@
/*--------------------------------------------------------------------------
@sinclair/typebox/extensions
@sinclair/typebox/experimental
The MIT License (MIT)
@@ -26,4 +26,6 @@ THE SOFTWARE.
---------------------------------------------------------------------------*/
export * from './experimental'
export * from './readonly-object'
export * from './union-enum'
export * from './union-oneof'

View File

@@ -1,30 +1,54 @@
# ExperimentalTypeBuilder
# Experimental Types
An experimental TypeBox type builder with additional custom types.
These examples are a set of experiemental candidate types that may introduced into TypeBox in future.
## Overview
## ReadonlyObject
The TypeBox TypeBuilder classes are designed to be extended with user defined types. Instances where you may wish to do this are if your application is dependent on custom schematics and/or non-JSON serializable values (an example of which might be a Mongo's `ObjectId` or other such non-serializable value)
## Application Type Builder
The following shows creating a simple `ApplicationTypeBuilder` with additional types `Nullable` and `StringEnum`. These types are fairly common in OpenAPI implementations.
Maps an object properties as `readonly`.
```typescript
import { StandardTypeBuilder, Static, TSchema } from '@sinclair/typebox'
import { ReadonlyObject } from './experimental'
export class ApplicationTypeBuilder extends StandardTypeBuilder { // only JSON Schema types
public Nullable<T extends TSchema>(schema: T) {
return this.Unsafe<Static<T> | null>({ ...schema, nullable: true })
}
public StringEnum<T extends string[]>(values: [...T]) {
return this.Unsafe<T[number]>({ type: 'string', enum: values })
}
}
const T = ReadonlyObject(Type.Object({
x: Type.Number()
}))
export const Type = new ApplicationTypeBuilder() // re-export!
type T = Static<typeof T> // type T = {
// readonly x: number
// }
```
## UnionEnum
## Experimental Type Builder
Creates an `enum` union string schema representation.
The `experimental.ts` file provided with this example shows advanced usage by creating complex types for potential inclusion in the TypeBox library in later revisions. It is offered for reference, experimentation and is open to contributor submission.
```typescript
import { UnionEnum } from './experimental'
const T = UnionEnum(['A', 'B', 'C']) // const T = {
// enum: ['A', 'B', 'C']
// }
type T = Static<typeof T> // type T = 'A' | 'B' | 'C'
```
## UnionOneOf
Creates a `oneOf` union representation.
```typescript
import { UnionOneOf } from './experimental'
const T = UnionOneOf([ // const T = {
Type.Literal('A'), // oneOf: [
Type.Literal('B'), // { const: 'A' },
Type.Literal('C') // { const: 'B' },
]) // { const: 'C' },
// ]
// }
type T = Static<typeof T> // type T = 'A' | 'B' | 'C'
```

View File

@@ -0,0 +1,68 @@
/*--------------------------------------------------------------------------
@sinclair/typebox/experimental
The MIT License (MIT)
Copyright (c) 2017-2023 Haydn Paterson (sinclair) <haydn.developer@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---------------------------------------------------------------------------*/
import * as Types from '@sinclair/typebox'
// -------------------------------------------------------------------------------------
// TReadonlyObject
// -------------------------------------------------------------------------------------
export type TReadonlyArray<T extends Types.TSchema[]> = Types.Assert<{ [K in keyof T]: TReadonlyObject<Types.Assert<T[K], Types.TSchema>> }, Types.TSchema[]>
// prettier-ignore
export type TReadonlyProperties<T extends Types.TProperties> = Types.Evaluate<Types.Assert<{
[K in keyof T]:
T[K] extends Types.TReadonlyOptional<infer U> ? Types.TReadonlyOptional<U> :
T[K] extends Types.TReadonly<infer U> ? Types.TReadonly<U> :
T[K] extends Types.TOptional<infer U> ? Types.TReadonlyOptional<U> :
Types.TReadonly<T[K]>
}, Types.TProperties>>
// prettier-ignore
export type TReadonlyObject<T extends Types.TSchema> =
T extends Types.TIntersect<infer S> ? Types.TIntersect<TReadonlyArray<S>> :
T extends Types.TUnion<infer S> ? Types.TUnion<TReadonlyArray<S>> :
T extends Types.TObject<infer S> ? Types.TObject<TReadonlyProperties<S>> :
Types.TReadonly<T>
/** `[Experimental]` Remaps a Intersect, Union or Object as readonly */
export function ReadonlyObject<T extends Types.TSchema>(schema: T): TReadonlyObject<T> {
function Apply(property: Types.TSchema): any {
// prettier-ignore
switch (property[Types.Modifier]) {
case 'ReadonlyOptional': property[Types.Modifier] = 'ReadonlyOptional'; break
case 'Readonly': property[Types.Modifier] = 'Readonly'; break
case 'Optional': property[Types.Modifier] = 'ReadonlyOptional'; break
default: property[Types.Modifier] = 'Readonly'; break
}
return property
}
// prettier-ignore
return (Types.TypeGuard.TIntersect(schema) || Types.TypeGuard.TUnion(schema) || Types.TypeGuard.TObject(schema))
? Types.ObjectMap.Map<TReadonlyObject<T>>(schema, (schema) => {
globalThis.Object.keys(schema.properties).forEach(key => Apply(schema.properties[key]))
return schema
}, {}) : Apply(schema)
}

View File

@@ -1,6 +1,6 @@
/*--------------------------------------------------------------------------
@sinclair/typebox/typedef
@sinclair/typebox/experimental
The MIT License (MIT)
@@ -26,4 +26,22 @@ THE SOFTWARE.
---------------------------------------------------------------------------*/
export * from './typedef'
import * as Types from '@sinclair/typebox'
// -------------------------------------------------------------------------------------
// TUnionEnum
// -------------------------------------------------------------------------------------
export interface TUnionEnum<T extends (string | number)[]> extends Types.TSchema {
[Types.Kind]: 'UnionEnum'
static: T[number]
enum: T
}
/** `[Experimental]` Creates a Union type with a `enum` schema representation */
export function UnionEnum<T extends (string | number)[]>(values: [...T], options: Types.SchemaOptions = {}) {
function UnionEnumCheck(schema: TUnionEnum<(string | number)[]>, value: unknown) {
return (typeof value === 'string' || typeof value === 'number') && schema.enum.includes(value)
}
if (!Types.TypeRegistry.Has('UnionEnum')) Types.TypeRegistry.Set('UnionEnum', UnionEnumCheck)
return { ...options, [Types.Kind]: 'UnionEnum', enum: values } as TUnionEnum<T>
}

View File

@@ -1,6 +1,6 @@
/*--------------------------------------------------------------------------
@sinclair/typebox/legacy
@sinclair/typebox/experimental
The MIT License (MIT)
@@ -26,4 +26,19 @@ THE SOFTWARE.
---------------------------------------------------------------------------*/
export * from './intersect'
import * as Types from '@sinclair/typebox'
import { Value } from '@sinclair/typebox/value'
export interface TUnionOneOf<T extends Types.TSchema[]> extends Types.TSchema {
[Types.Kind]: 'UnionOneOf'
static: { [K in keyof T]: Types.Static<T[K]> }[number]
oneOf: T
}
/** `[Experimental]` Creates a Union type with a `oneOf` schema representation */
export function UnionOneOf<T extends Types.TSchema[]>(oneOf: [...T], options: Types.SchemaOptions = {}) {
function UnionOneOfCheck(schema: TUnionOneOf<Types.TSchema[]>, value: unknown) {
return 1 === schema.oneOf.reduce((acc: number, schema: any) => (Value.Check(schema, value) ? acc + 1 : acc), 0)
}
if (!Types.TypeRegistry.Has('UnionOneOf')) Types.TypeRegistry.Set('UnionOneOf', UnionOneOfCheck)
return { ...options, [Types.Kind]: 'UnionOneOf', oneOf } as TUnionOneOf<T>
}

View File

@@ -1,35 +1,11 @@
# String Formats
# Formats
TypeBox does not implement any string formats by default. However it is possible to register user defined formats using the `FormatRegistry`. Once registered, the format becomes available to both `Value` and `TypeCompiler` modules.
This example provides TypeCompiler supported versions of the `ajv-formats` package.
## FormatRegistry
The following shows basic usage of the format registry
```typescript
import { Type, FormatRegistry } from '@sinclair/typebox'
import { Value } from '@sinclair/typebox/value'
// Register the 'foo-only' format. The format checks for 'foo' only.
FormatRegistry.Set('foo-only', value => value === 'foo')
const T = Type.String({ format: 'foo-only' })
// Validate
Value.Check(T, 'foo') // true
Value.Check(T, 'bar') // false
```
## Standard Formats
## Standard
The `standard.ts` file provided with this example implements several standard string formats.
```typescript
import './standard'
```
The following formats are implemented by `standard.ts`
| Format | Description |
| --- | --- |
| `email` | Internet email address, see [RFC 5321, section 4.1.2.](http://tools.ietf.org/html/rfc5321#section-4.1.2) |

View File

@@ -1,81 +1,266 @@
# TypeDef
TypeBox may offer support for [RPC8927](https://www.rfc-editor.org/rfc/rfc8927) JSON Type Definition in future revisions of the library. This specification is much simpler than JSON Schema but can be useful when describing schematics that need to be shared with nominal type languages.
TypeBox is considering support for the JSON Type Definition [RFC8927](https://www.rfc-editor.org/rfc/rfc8927) specification in future releases. This specification is similar to JSON Schema but provides a constrained type representation that enables schematics to map more naturally to [nominal type systems](https://en.wikipedia.org/wiki/Nominal_type_system) as well as offering type primitives such as `int8`, `uint32` or `float32`. JSON Type Definition can be useful in applications that need to express and share data structures in a way that can be understood by a wide range of programming languages outside of JavaScript.
License MIT
## Contents
- [Usage](#Usage)
- [Types](#Types)
- [Unions](#Unions)
- [Check](#Check)
## Usage
The file `typedef.ts` provided with this example contains the provisional implementation for RPC8927.
TypeBox currently doesn't publish TypeDef as part of the mainline package. However the TypeDef functionality is written to be a standalone module you can copy into your project. You will also need `@sinclair/typebox` installed. You can obtain the `typedef` module from `example/typedef/typedef.ts` contained within this repository.
```typescript
import { Type, Static } from './typedef'
const T = Type.Struct({ // const T = {
x: Type.Float32(), // properties: {
y: Type.Float32(), // x: { type: 'float32' },
z: Type.Float32() // y: { type: 'float32' },
}) // z: { type: 'float32' }
// }
// }
type T = Static<typeof T> // type T = {
// x: number,
// y: number,
// z: number
// }
```
## Types
The following types are supported by the typedef module. Please note these types are not compatible with the JSON Schema specification and should not be combined with the standard TypeBox types.
```typescript
┌────────────────────────────────┬─────────────────────────────┬────────────────────────────────┐
TypeBox TypeScript JSON Type Definition
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Boolean() type T = boolean const T = {
type: 'boolean'
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.String() type T = string const T = {
type: 'string'
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Float32() type T = number const T = {
type: 'float32'
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Float64() type T = number const T = {
type: 'float64'
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Int8() type T = number const T = {
type: 'int8'
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Int16() type T = number const T = {
type: 'int16'
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Int32() type T = number const T = {
type: 'int32'
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Uint8() type T = number const T = {
type: 'uint8'
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Uint16() type T = number const T = {
type: 'uint16'
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Uint32() type T = number const T = {
type: 'uint32'
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Timestamp() type T = number const T = {
type: 'timestamp'
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Struct([ type T = { const T = {
x: Type.Float32(), x: number, properties: {
y: Type.Float32(), y: number x: number,
]) } y: number
}
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Array( type T = number[] const T = {
Type.Float32() elements: {
) type: 'float32'
}
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Record( type T = Record< const T = {
Type.Float32() string, values: {
) number type: 'float32'
> }
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Enum([ type T = 'A' | 'B' | 'C' const T = {
'A', 'B', 'C' enum: [
]) 'A',
'B',
'C'
]
}
├────────────────────────────────┼─────────────────────────────┼────────────────────────────────┤
const T = Type.Union([ type T = { const T = {
Type.Struct({ kind: '0', discriminator: 'kind',
x: Type.Float32() x: number mapping: {
}), } | { '0': {
Type.Struct({ kind: '1' properties: {
y: Type.Float32() y: number x: {
]) } type: 'float32'
], 'kind') }
}
},
'1': { |
properties: {
y: {
type: 'float32'
}
}
}
}
}
└────────────────────────────────┴─────────────────────────────┴────────────────────────────────┘
```
## Unions
TypeBox supports JSON Type Definition discriminated unions with `Type.Union`. This type works similar its JSON Schema counterpart, but can only accept types of `Type.Struct` and will infer each struct with an additional named `discriminator` field. The representation for discriminated unions are also quite different, where instead of `anyOf` or `oneOf`, a set of `mapping` properties are used for each sub type.
```typescript
const Vector2 = Type.Struct({ // const Vector2 = {
x: Type.Float32(), // properties: {
y: Type.Float32() // x: { type: 'float32' },
}) // y: { type: 'float32' }
// }
// }
const Vector3 = Type.Struct({ // const Vector3 = {
x: Type.Float32(), // properties: {
y: Type.Float32(), // x: { type: 'float32' },
z: Type.Float32() // y: { type: 'float32' },
}) // z: { type: 'float32' }
// }
// }
const Vector4 = Type.Struct({ // const Vector4 = {
x: Type.Float32(), // properties: {
y: Type.Float32(), // x: { type: 'float32' },
z: Type.Float32(), // y: { type: 'float32' },
w: Type.Float32() // z: { type: 'float32' },
}) // w: { type: 'float32' }
// }
// }
const T = Type.Union([ // const T = {
Vector2, // discriminator: 'type',
Vector3, // mapping: {
Vector4 // 0: {
]) // properties: {
// x: { type: 'float32' },
// y: { type: 'float32' }
// }
// },
// 1: {
// properties: {
// x: { type: 'float32' },
// y: { type: 'float32' },
// z: { type: 'float32' }
// }
// },
// 2: {
// properties: {
// x: { type: 'float32' },
// y: { type: 'float32' },
// z: { type: 'float32' }
// }
// }
// }
// }
type T = Static<typeof T> // type T = {
// type: '0',
// x: number,
// y: number
// } | {
// type: '1',
// x: number,
// y: number,
// y: number
// } | {
// type: '2',
// x: number,
// y: number,
// y: number,
// w: number
// }
```
To type check a value matching the above union, the value will need to contain the discriminator property `type` with a value matching one of the sub type `mapping` keys. The inference type shown above can be a good reference point to understand the structure of the expected value. Nominal type systems will use the discriminator to an expected target type.
The following are examples of valid and invalid union data.
```typescript
const V = { x: 1, y: 1 } // invalid Vector2
const V = { type: '0', x: 1, y: 1 } // valid Vector2
const V = { type: '0', x: 1, y: 1, z: 1 } // invalid Vector2
const V = { type: '1', x: 1, y: 1, z: 1 } // valid Vector3
```
## Check
TypeDef types are partially supported with the `TypeCompiler` and `Value` checking modules through the extensible type system in TypeBox. Please note these types are not optimized for JIT performance and do not provide deep error reporting support. For more fully featured validation support consider Ajv. Documentation of Ajv support can be found [here](https://ajv.js.org/json-type-definition.html).
The following is TypeDef used with TypeBox's type checking infrastructure.
```typescript
import { TypeCompiler } from '@sinclair/typebox/compiler'
import { Value } from '@sinclair/typebox/value'
import { Type, Static } from './typedef'
const T = Type.Struct('T', { // const T = {
x: Type.Float32(), // properties: {
y: Type.Float32(), // x: { type: "float32" },
z: Type.Float32() // y: { type: 'float32' },
}) // z: { type: 'float32' }
// }
// }
const T = Type.Struct({
x: Type.Float32(),
y: Type.Float32(),
z: Type.Float32()
})
type T = Static<typeof T> // type T = {
// x: number,
// z: number,
// y: number
// }
const V = {
x: 1,
y: 2,
z: 3
}
const R = Value.Check(T, { x: 1, y: 2, z: 3 }) // const R = true
```
const R1 = TypeCompiler.Compile(T).Check(V) // true
### Unions
The JSON Type Definition has a different representation for unions and is primarily orientated towards discriminated unions. To use unions, you will need to name each struct on the first argument. TypeBox will take care of producing the union representation and static type.
```typescript
import { Type, Static } from './typedef'
const Vector2 = Type.Struct('Vector2', { // const Vector2 = {
x: Type.Float32(), // properties: {
y: Type.Float32(), // x: { type: 'float32' },
}) // y: { type: 'float32' }
// }
// }
const Vector3 = Type.Struct('Vector3', { // const Vector3 = {
x: Type.Float32(), // properties: {
y: Type.Float32(), // x: { type: 'float32' },
z: Type.Float32() // y: { type: 'float32' },
}) // z: { type: 'float32' }
// }
// }
const Union = Type.Union('type', [ // const Union = {
Vector2, // discriminator: 'type',
Vector3 // mapping: {
]) // Vector2: {
// properties: {
// x: { type: 'float32' },
// y: { type: 'float32' },
// }
// },
// Vector3: {
// properties: {
// x: { type: 'float32' },
// y: { type: 'float32' },
// z: { type: 'float32' }
// }
// }
// }
// }
type Union = Static<typeof Union> // type Union = {
// type: 'Vector2'
// x: number
// y: number
// } | {
// type: 'Vector3'
// x: number
// y: number
// z: number
// }
```
const R2 = Value.Check(T, V) // true
```

View File

@@ -26,13 +26,20 @@ THE SOFTWARE.
---------------------------------------------------------------------------*/
export { type Static, TSchema, PropertiesReduce, TReadonly, TReadonlyOptional, TOptional } from '@sinclair/typebox'
export { Static, Evaluate, TSchema, PropertiesReduce, TReadonly, TReadonlyOptional, TOptional } from '@sinclair/typebox'
import * as Types from '@sinclair/typebox'
// --------------------------------------------------------------------------
// Symbols
// Utility Types
// --------------------------------------------------------------------------
export const Name = Symbol.for('TypeBox:Name')
export type Assert<T, U> = T extends U ? T : never
export type Base = { m: string, t: string }
export type Base16 = { m: 'F', t: '01', '0': '1', '1': '2', '2': '3', '3': '4', '4': '5', '5': '6', '6': '7', '7': '8', '8': '9', '9': 'A', 'A': 'B', 'B': 'C', 'C': 'D', 'D': 'E', 'E': 'F', 'F': '0' }
export type Base10 = { m: '9', t: '01', '0': '1', '1': '2', '2': '3', '3': '4', '4': '5', '5': '6', '6': '7', '7': '8', '8': '9', '9': '0' }
export type Reverse<T extends string> = T extends `${infer L}${infer R}` ? `${Reverse<R>}${L}` : T
export type Tick<T extends string, B extends Base> = T extends keyof B ? B[T] : never
export type Next<T extends string, B extends Base> = T extends Assert<B, Base>['m'] ? Assert<B, Base>['t'] : T extends `${infer L}${infer R}` ? L extends Assert<B, Base>['m'] ? `${Assert<Tick<L, B>, string>}${Next<R, B>}` : `${Assert<Tick<L, B>, string>}${R}` : never
export type Increment<T extends string, B extends Base = Base10> = Reverse<Next<Reverse<T>, B>>
// --------------------------------------------------------------------------
// TArray
// --------------------------------------------------------------------------
@@ -50,6 +57,19 @@ export interface TBoolean extends Types.TSchema {
type: 'boolean'
}
// --------------------------------------------------------------------------
// TUnion
// --------------------------------------------------------------------------
type InferUnion<T extends TStruct[], D extends string, Index = string> = T extends [infer L, ...infer R]
? Types.Evaluate<{ [_ in D]: Index } & Types.Static<Types.AssertType<L>>> | InferUnion<Types.AssertRest<R>, D, Increment<Types.Assert<Index, string>>>
: never
export interface TUnion<T extends TStruct[] = TStruct[], D extends string = string> extends Types.TSchema {
[Types.Kind]: 'TypeDef:Union'
static: InferUnion<T, D, '0'>
discriminator: D,
mapping: T
}
// --------------------------------------------------------------------------
// TEnum
// --------------------------------------------------------------------------
export interface TEnum<T extends string[] = string[]> extends Types.TSchema {
@@ -60,7 +80,7 @@ export interface TEnum<T extends string[] = string[]> extends Types.TSchema {
// --------------------------------------------------------------------------
// TFloat32
// --------------------------------------------------------------------------
export interface TFloat32 extends Types.TSchema {
export interface TFloat32 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Float32'
type: 'float32'
static: number
@@ -68,7 +88,7 @@ export interface TFloat32 extends Types.TSchema {
// --------------------------------------------------------------------------
// TFloat64
// --------------------------------------------------------------------------
export interface TFloat64 extends Types.TSchema {
export interface TFloat64 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Float64'
type: 'float64'
static: number
@@ -76,7 +96,7 @@ export interface TFloat64 extends Types.TSchema {
// --------------------------------------------------------------------------
// TInt8
// --------------------------------------------------------------------------
export interface TInt8 extends Types.TSchema {
export interface TInt8 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Int8'
type: 'int8'
static: number
@@ -84,7 +104,7 @@ export interface TInt8 extends Types.TSchema {
// --------------------------------------------------------------------------
// TInt16
// --------------------------------------------------------------------------
export interface TInt16 extends Types.TSchema {
export interface TInt16 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Int16'
type: 'int16'
static: number
@@ -92,7 +112,7 @@ export interface TInt16 extends Types.TSchema {
// --------------------------------------------------------------------------
// TInt32
// --------------------------------------------------------------------------
export interface TInt32 extends Types.TSchema {
export interface TInt32 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Int32'
type: 'int32'
static: number
@@ -100,7 +120,7 @@ export interface TInt32 extends Types.TSchema {
// --------------------------------------------------------------------------
// TUint8
// --------------------------------------------------------------------------
export interface TUint8 extends Types.TSchema {
export interface TUint8 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Uint8'
type: 'uint8'
static: number
@@ -108,7 +128,7 @@ export interface TUint8 extends Types.TSchema {
// --------------------------------------------------------------------------
// TUint16
// --------------------------------------------------------------------------
export interface TUint16 extends Types.TSchema {
export interface TUint16 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Uint16'
type: 'uint16'
static: number
@@ -148,12 +168,11 @@ type RequiredKeys<T extends TFields> = { [K in keyof T]: T[K] extends (Types.TRe
export interface StructOptions {
additionalProperties?: boolean
}
export interface TStruct<D extends string = string, T extends TFields = TFields> extends Types.TSchema, StructOptions {
[Name]: D
export interface TStruct<T extends TFields = TFields> extends Types.TSchema, StructOptions {
[Types.Kind]: 'TypeDef:Struct'
static: Types.PropertiesReduce<T, this['params']>
optionalProperties: {[K in Types.Assert<OptionalKeys<T>, keyof T>]: T[K] }
properties: {[K in Types.Assert<RequiredKeys<T>, keyof T>]: T[K] }
optionalProperties: { [K in Types.Assert<OptionalKeys<T>, keyof T>]: T[K] }
properties: { [K in Types.Assert<RequiredKeys<T>, keyof T>]: T[K] }
}
// --------------------------------------------------------------------------
// TTimestamp
@@ -163,43 +182,34 @@ export interface TTimestamp extends Types.TSchema {
static: number
}
// --------------------------------------------------------------------------
// TUnion
// --------------------------------------------------------------------------
export interface TUnion<D extends string = string, T extends TStruct[] = TStruct[]> extends Types.TSchema {
[Types.Kind]: 'TypeDef:Union'
static: Types.Evaluate<{ [K in keyof T]: { [key in D]: T[K][typeof Name] } & Types.Static<T[K]> }[number]>
discriminator: D,
mapping: T
}
// --------------------------------------------------------------------------
// TypeRegistry
// --------------------------------------------------------------------------
Types.TypeRegistry.Set<TArray>('TypeDef:Array', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TBoolean>('TypeDef:Boolean', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TInt8>('TypeDef:Int8', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TInt16>('TypeDef:Int16', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TInt32>('TypeDef:Int32', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TUint8>('TypeDef:Uint8', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TUint16>('TypeDef:Uint16', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TUint32>('TypeDef:Uint32', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TRecord>('TypeDef:Record', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TString>('TypeDef:String', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TStruct>('TypeDef:Struct', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TTimestamp>('TypeDef:Timestamp', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TUnion>('TypeDef:Union', (schema, value) => TypeDefCheck.Check(schema, value))
Types.TypeRegistry.Set<TArray>('TypeDef:Array', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TBoolean>('TypeDef:Boolean', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TUnion>('TypeDef:Union', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TInt8>('TypeDef:Int8', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TInt16>('TypeDef:Int16', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TInt32>('TypeDef:Int32', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TUint8>('TypeDef:Uint8', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TUint16>('TypeDef:Uint16', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TUint32>('TypeDef:Uint32', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TRecord>('TypeDef:Record', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TString>('TypeDef:String', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TStruct>('TypeDef:Struct', (schema, value) => ValueCheck.Check(schema, value))
Types.TypeRegistry.Set<TTimestamp>('TypeDef:Timestamp', (schema, value) => ValueCheck.Check(schema, value))
// --------------------------------------------------------------------------
// TypeDefCheck
// ValueCheck
// --------------------------------------------------------------------------
export class TypeDefCheckUnionTypeError extends Error {
export class ValueCheckError extends Error {
constructor(public readonly schema: Types.TSchema) {
super('TypeDefCheck: Unknown type')
super('ValueCheck: Unknown type')
}
}
export namespace TypeDefCheck {
export namespace ValueCheck {
// ------------------------------------------------------------------------
// Guards
// ------------------------------------------------------------------------
function IsObject(value: unknown): value is Record<keyof any, unknown> {
function IsObject(value: unknown): value is Record<keyof any, any> {
return typeof value === 'object' && value !== null && !globalThis.Array.isArray(value)
}
function IsArray(value: unknown): value is unknown[] {
@@ -253,37 +263,46 @@ export namespace TypeDefCheck {
function String(schema: TString, value: unknown): boolean {
return typeof value === 'string'
}
function Struct(schema: TStruct, value: unknown): boolean {
function Struct(schema: TStruct, value: unknown, descriminator?: string): boolean {
if (!IsObject(value)) return false
const optionalKeys = schema.optionalProperties === undefined ? [] : globalThis.Object.getOwnPropertyNames(schema.optionalProperties)
const requiredKeys = schema.properties === undefined ? [] : globalThis.Object.getOwnPropertyNames(schema.properties)
if(!(IsObject(value) &&
optionalKeys.every(key => key in value ? Visit((schema.optionalProperties as any)[key], value[key]) : true) &&
requiredKeys.every(key => key in value && Visit(((schema as any).properties[key] as any), value[key])))) return false
if(schema.additionalProperties === true) return true
const unknownKeys = globalThis.Object.getOwnPropertyNames(value)
return unknownKeys.every(key => optionalKeys.includes(key) || requiredKeys.includes(key))
for (const requiredKey of requiredKeys) {
if (!(requiredKey in value)) return false
const requiredProperty = value[requiredKey]
const requiredSchema = (schema as any).properties[requiredKey]
if (!Visit(requiredSchema, requiredProperty)) return false
}
for (const optionalKey of optionalKeys) {
if (!(optionalKey in value)) continue
const optionalProperty = value[optionalKey]
const optionalSchema = (schema as any).properties[optionalKey]
if (!Visit(optionalSchema, optionalProperty)) return false
}
if (schema.additionalProperties === true) return true
const knownKeys = [...optionalKeys, ...requiredKeys]
for (const unknownKey of unknownKeys) if (!knownKeys.includes(unknownKey) && (descriminator !== undefined && unknownKey !== descriminator)) return false
for (const knownKey of knownKeys) if (!unknownKeys.includes(knownKey)) return false
return true
}
function Timestamp(schema: TString, value: unknown): boolean {
return IsInt(value, 0, Number.MAX_SAFE_INTEGER)
}
function Union(schema: TUnion, value: unknown): boolean {
if (!(
IsObject(value) &&
schema.discriminator in value &&
IsString(value[schema.discriminator]) &&
value[schema.discriminator] as any in schema.mapping
)) return false
// We shouldn't create objects just to omit the discriminator (optimize)
const inner = globalThis.Object.keys(value).reduce((acc, key) => {
return key === schema.discriminator ? acc : { [key]: value[key] }
}, {})
return Visit(schema.mapping[value[schema.discriminator] as any], inner)
if (!IsObject(value)) return false
if (!(schema.discriminator in value)) return false
if (!IsString(value[schema.discriminator])) return false
if (!(value[schema.discriminator] in schema.mapping)) return false
const struct = schema.mapping[value[schema.discriminator]] as TStruct
return Struct(struct, value, schema.discriminator)
}
function Visit(schema: Types.TSchema, value: unknown): boolean {
const anySchema = schema as any
switch(anySchema[Types.Kind]) {
switch (anySchema[Types.Kind]) {
case 'TypeDef:Array': return Array(anySchema, value)
case 'TypeDef:Boolean': return Boolean(anySchema, value)
case 'TypeDef:Union': return Union(anySchema, value)
case 'TypeDef:Enum': return Enum(anySchema, value)
case 'TypeDef:Float32': return Float32(anySchema, value)
case 'TypeDef:Float64': return Float64(anySchema, value)
@@ -297,11 +316,10 @@ export namespace TypeDefCheck {
case 'TypeDef:String': return String(anySchema, value)
case 'TypeDef:Struct': return Struct(anySchema, value)
case 'TypeDef:Timestamp': return Timestamp(anySchema, value)
case 'TypeDef:Union': return Union(anySchema, value)
default: throw new TypeDefCheckUnionTypeError(anySchema)
default: throw new ValueCheckError(anySchema)
}
}
export function Check(schema: Types.TSchema, value: unknown): boolean {
export function Check<T extends Types.TSchema>(schema: T, value: unknown): value is Types.Static<T> {
return Visit(schema, value)
}
}
@@ -325,83 +343,79 @@ export class TypeDefTypeBuilder extends Types.TypeBuilder {
return { [Types.Modifier]: 'Readonly', ...schema }
}
// ------------------------------------------------------------------------
// Modifiers
// Types
// ------------------------------------------------------------------------
/** `[Standard]` Creates a TypeDef Array type */
/** [Standard] Creates a Array type */
public Array<T extends Types.TSchema>(elements: T): TArray<T> {
return this.Create({ [Types.Kind]: 'TypeDef:Array', elements })
}
/** `[Standard]` Creates a TypeDef Boolean type */
/** [Standard] Creates a Boolean type */
public Boolean(): TBoolean {
return this.Create({ [Types.Kind]: 'TypeDef:Boolean', type: 'boolean' })
}
/** `[Standard]` Creates a TypeDef Enum type */
/** [Standard] Creates a Enum type */
public Enum<T extends string[]>(values: [...T]): TEnum<T> {
return this.Create({ [Types.Kind]: 'TypeDef:Enum', enum: values })
}
/** `[Standard]` Creates a TypeDef Float32 type */
/** [Standard] Creates a Float32 type */
public Float32(): TFloat32 {
return this.Create({ [Types.Kind]: 'TypeDef:Float32', type: 'float32' })
}
/** `[Standard]` Creates a TypeDef Float64 type */
/** [Standard] Creates a Float64 type */
public Float64(): TFloat64 {
return this.Create({ [Types.Kind]: 'TypeDef:Float64', type: 'float64' })
}
/** `[Standard]` Creates a TypeDef Int8 type */
/** [Standard] Creates a Int8 type */
public Int8(): TInt8 {
return this.Create({ [Types.Kind]: 'TypeDef:Int8', type: 'int8' })
}
/** `[Standard]` Creates a TypeDef Int16 type */
/** [Standard] Creates a Int16 type */
public Int16(): TInt16 {
return this.Create({ [Types.Kind]: 'TypeDef:Int16', type: 'int16' })
}
/** `[Standard]` Creates a TypeDef Int32 type */
/** [Standard] Creates a Int32 type */
public Int32(): TInt32 {
return this.Create({ [Types.Kind]: 'TypeDef:Int32', type: 'int32' })
}
/** `[Standard]` Creates a TypeDef Uint8 type */
/** [Standard] Creates a Uint8 type */
public Uint8(): TUint8 {
return this.Create({ [Types.Kind]: 'TypeDef:Uint8', type: 'uint8' })
}
/** `[Standard]` Creates a TypeDef Uint16 type */
/** [Standard] Creates a Uint16 type */
public Uint16(): TUint16 {
return this.Create({ [Types.Kind]: 'TypeDef:Uint16', type: 'uint16' })
}
/** `[Standard]` Creates a TypeDef Uint32 type */
/** [Standard] Creates a Uint32 type */
public Uint32(): TUint32 {
return this.Create({ [Types.Kind]: 'TypeDef:Uint32', type: 'uint32' })
}
/** `[Standard]` Creates a TypeDef Record type */
/** [Standard] Creates a Record type */
public Record<T extends Types.TSchema>(values: T): TRecord<T> {
return this.Create({ [Types.Kind]: 'TypeDef:Record',values })
return this.Create({ [Types.Kind]: 'TypeDef:Record', values })
}
/** `[Standard]` Creates a TypeDef String type */
/** [Standard] Creates a String type */
public String(): TString {
return this.Create({ [Types.Kind]: 'TypeDef:String',type: 'string' })
}
/** `[Standard]` Creates a TypeDef Struct type */
public Struct<N extends string, T extends TFields>(name: N, fields: T, options?: StructOptions): TStruct<N, T> {
const optionalProperties = globalThis.Object.getOwnPropertyNames(fields).reduce((acc, key) => (Types.TypeGuard.TOptional(fields[key]) || Types.TypeGuard.TReadonlyOptional(fields[key]) ? { ...acc, [key]: fields[key] } : { ...acc }), {} as TFields)
const properties = globalThis.Object.getOwnPropertyNames(fields).reduce((acc, key) => (Types.TypeGuard.TOptional(fields[key]) || Types.TypeGuard.TReadonlyOptional(fields[key]) ? {... acc } : { ...acc, [key]: fields[key] }), {} as TFields)
const optionalPropertiesObject = globalThis.Object.getOwnPropertyNames(optionalProperties).length > 0 ? { optionalProperties: optionalProperties } : {}
const propertiesObject = globalThis.Object.getOwnPropertyNames(properties).length === 0 ? {} : { properties: properties }
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Struct', [Name]: name, ...propertiesObject, ...optionalPropertiesObject })
return this.Create({ [Types.Kind]: 'TypeDef:String', type: 'string' })
}
/** `[Standard]` Creates a TypeDef Timestamp type */
/** [Standard] Creates a Struct type */
public Struct<T extends TFields>(fields: T, options?: StructOptions): TStruct<T> {
const optionalProperties = globalThis.Object.getOwnPropertyNames(fields).reduce((acc, key) => (Types.TypeGuard.TOptional(fields[key]) || Types.TypeGuard.TReadonlyOptional(fields[key]) ? { ...acc, [key]: fields[key] } : { ...acc }), {} as TFields)
const properties = globalThis.Object.getOwnPropertyNames(fields).reduce((acc, key) => (Types.TypeGuard.TOptional(fields[key]) || Types.TypeGuard.TReadonlyOptional(fields[key]) ? { ...acc } : { ...acc, [key]: fields[key] }), {} as TFields)
const optionalObject = globalThis.Object.getOwnPropertyNames(optionalProperties).length > 0 ? { optionalProperties: optionalProperties } : {}
const requiredObject = globalThis.Object.getOwnPropertyNames(properties).length === 0 ? {} : { properties: properties }
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Struct', ...requiredObject, ...optionalObject })
}
/** [Standard] Creates a Union type */
public Union<T extends TStruct<TFields>[], D extends string = 'type'>(structs: [...T], discriminator?: D): TUnion<T, D> {
discriminator = (discriminator || 'type') as D
if (structs.length === 0) throw new Error('TypeDefTypeBuilder: Union types must contain at least one struct')
const mapping = structs.reduce((acc, current, index) => ({ ...acc, [index.toString()]: current }), {})
return this.Create({ [Types.Kind]: 'TypeDef:Union', discriminator, mapping })
}
/** [Standard] Creates a Timestamp type */
public Timestamp(): TTimestamp {
return this.Create({ [Types.Kind]: 'TypeDef:Timestamp', type: 'timestamp' })
}
/** `[Standard]` Creates a TypeDef Discriminated Union type */
public Union<D extends string, T extends TStruct<string, TFields>[]>(discriminator: D, objects: [...T]): TUnion<D, T> {
if(objects.length === 0) throw new Error('TypeDefTypeBuilder: Union types must have at least one object')
const exists = objects.every(object => typeof object[Name] === 'string')
if(!exists) throw new Error('TypeDefTypeBuilder: All union objects MUST have a descriminator')
const unique = objects.reduce((set, current) => set.add(current[Name]), new Set<string>())
if(unique.size !== objects.length) throw new Error('TypeDefTypeBuilder: All union objects MUST unique descriminator strings')
const mapping = objects.reduce((acc, current) => ({ ...acc, [current[Name]]: current }), {})
return this.Create({ [Types.Kind]: 'TypeDef:Union', discriminator, mapping })
}
}
/** JSON Type Definition Type Builder */

View File

@@ -1,7 +0,0 @@
# TypeMap
TypeMap is a fluent type builder abstraction built on top of TypeBox. It is modelled after the Yup and Zod and libraries, but internally uses TypeBox for type composition, inference and runtime type assertion. It supports the same advanced compositional types as TypeBox (including generics, conditional, recursive and template literal types), but offers this functionality as a set of chainable lowercase types.
Like TypeBox, TypeMap internally uses JSON schema for its type representation. It provides access to the TypeBox compiler infrastructure with the `.compile()` and `.code()` methods which are available on all types, and access to the internal schema via `.schema()`. Types also implement `.check()` and `.cast()` for convenience.
TypeMap is implemented as an example for reference purposes only.

View File

@@ -1,496 +0,0 @@
/*--------------------------------------------------------------------------
@sinclair/typebox/typemap
The MIT License (MIT)
Copyright (c) 2017-2023 Haydn Paterson (sinclair) <haydn.developer@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---------------------------------------------------------------------------*/
import { TypeCompiler, ValueError, TypeCheck } from '@sinclair/typebox/compiler'
import { Value, ValueErrorIterator } from '@sinclair/typebox/value'
import { TypeSystem } from '@sinclair/typebox/system'
import * as T from '@sinclair/typebox'
// -------------------------------------------------------------------------------------------------
// Formats
// -------------------------------------------------------------------------------------------------
// prettier-ignore
TypeSystem.Format('email', (value) => /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(value))
// prettier-ignore
TypeSystem.Format('uuid', (value) => /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(value))
// prettier-ignore
TypeSystem.Format('url', (value) => /^(?:https?|wss?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu.test(value))
// prettier-ignore
TypeSystem.Format('ipv6', (value) => /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i.test(value))
// prettier-ignore
TypeSystem.Format('ipv4', (value) => /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/.test(value))
// -------------------------------------------------------------------------------------------------
// Type Mapping
// -------------------------------------------------------------------------------------------------
export type TypeToType<T extends Type> = T extends Type<infer S> ? S : never
export type TypeToTuple<T extends Type[]> = {
[K in keyof T]: T[K] extends Type<infer S> ? S : never
}
export type TypeToProperties<T extends PropertiesType> = T.Assert<
{
[K in keyof T]: T[K] extends Type<infer S> ? S : never
},
T.TProperties
>
export type PropertiesType = Record<keyof any, Type>
// prettier-ignore
export type TemplateLiteralType =
| Type<T.TUnion>
| Type<T.TLiteral>
| Type<T.TInteger>
| Type<T.TTemplateLiteral>
| Type<T.TNumber>
| Type<T.TBigInt>
| Type<T.TString>
| Type<T.TBoolean>
| Type<T.TNever>;
// -------------------------------------------------------------------------------------------------
// Error
// -------------------------------------------------------------------------------------------------
export class TypeValueError extends Error {
constructor(public readonly errors: ValueError[]) {
super('TypeValueError: Invalid Value')
}
}
// -------------------------------------------------------------------------------------------------
// Assert
// -------------------------------------------------------------------------------------------------
export interface TypeAssert<T extends T.TSchema> {
check(value: unknown): value is T.Static<T>
errors(value: unknown): ValueErrorIterator
code(): string
}
export class TypeAssertDynamic<T extends T.TSchema> implements TypeAssert<T> {
readonly #schema: T
constructor(schema: T) {
this.#schema = schema
}
public check(value: unknown): value is T.Static<T, []> {
return Value.Check(this.#schema, [], value)
}
public errors(value: unknown): ValueErrorIterator {
return Value.Errors(this.#schema, [], value)
}
public code(): string {
return TypeCompiler.Code(this.#schema)
}
}
export class TypeAssertCompiled<T extends T.TSchema> implements TypeAssert<T> {
readonly #typecheck: TypeCheck<T>
constructor(schema: T) {
this.#typecheck = TypeCompiler.Compile(schema)
}
public check(value: unknown): value is T.Static<T, []> {
return this.#typecheck.Check(value)
}
public errors(value: unknown): ValueErrorIterator {
return this.#typecheck.Errors(value)
}
public code(): string {
return this.#typecheck.Code()
}
}
// -----------------------------------------------------------------------------------------
// Type
// -----------------------------------------------------------------------------------------
export class Type<T extends T.TSchema = T.TSchema> {
#assert: TypeAssert<T>
#schema: T
constructor(schema: T) {
this.#assert = new TypeAssertDynamic(schema)
this.#schema = schema
}
/** Augments this type with the given options */
public options(options: T.SchemaOptions) {
return new Type({ ...this.schema, ...options } as T)
}
/** Maps a property as readonly and optional */
public readonlyOptional(): Type<T.TReadonlyOptional<T>> {
return new Type(T.Type.ReadonlyOptional(this.#schema))
}
/** Maps a property as optional */
public optional(): Type<T.TOptional<T>> {
return new Type(T.Type.Optional(this.#schema))
}
/** Maps a property as readonly */
public readonly(): Type<T.TReadonly<T>> {
return new Type(T.Type.Readonly(this.#schema))
}
/** Composes this type as a intersect with the given type */
public and<U extends Type>(type: U): Type<T.TIntersect<[T, TypeToType<U>]>> {
return new Type(T.Type.Intersect([this.#schema, type.schema()])) as any
}
/** Composes this type as a union with the given type */
public or<U extends Type>(type: U): Type<T.TUnion<[T, TypeToType<U>]>> {
return new Type(T.Type.Union([this.#schema, type.schema()])) as any
}
/** Picks the given properties from this type */
public pick<K extends (keyof T.Static<T>)[]>(keys: readonly [...K]): Type<T.TPick<T, K[number]>>
/** Picks the given properties from this type */
public pick<K extends T.TUnion<T.TLiteral<string>[]>>(keys: K): Type<T.TPick<T, T.TUnionOfLiteral<K>>>
/** Picks the given properties from this type */
public pick<K extends T.TLiteral<string>>(key: K): Type<T.TPick<T, K['const']>>
/** Picks the given properties from this type */
public pick<K extends T.TNever>(key: K): Type<T.TPick<T, never>> {
return new Type(T.Type.Pick(this.#schema, key))
}
/** Omits the given properties from this type */
public omit<K extends (keyof T.Static<T>)[]>(keys: readonly [...K]): Type<T.TOmit<T, K[number]>>
/** Omits the given properties from this type */
public omit<K extends T.TUnion<T.TLiteral<string>[]>>(keys: K): Type<T.TOmit<T, T.TUnionOfLiteral<K>>>
/** Omits the given properties from this type */
public omit<K extends T.TLiteral<string>>(key: K): Type<T.TOmit<T, K['const']>>
/** Omits the given properties from this type */
public omit<K extends T.TNever>(key: K): Type<T.TOmit<T, never>> {
return new Type(T.Type.Omit(this.#schema, key))
}
/** Applies partial to this type */
public partial(): Type<T.TPartial<T>> {
return new Type(T.Type.Partial(this.#schema))
}
/** Applies required to this type */
public required(): Type<T.TRequired<T>> {
return new Type(T.Type.Required(this.schema()))
}
/** Returns the keys of this type */
public keyof(): Type<T.TKeyOf<T>> {
return new Type(T.Type.KeyOf(this.schema()))
}
/** Checks this value and throws if invalid */
public assert(value: unknown): void {
if (this.#assert.check(value)) return
throw new TypeValueError([...this.#assert.errors(value)])
}
/** Casts this value into this type */
public cast(value: unknown): T.Static<T> {
return Value.Cast(this.#schema, [], value)
}
/** Returns true if this value is valid for this type */
public check(value: unknown): value is T.Static<T> {
return this.#assert.check(value)
}
/** Returns the assertion code for this type */
public code(): string {
return this.#assert.code()
}
/** Creates a default value for this type */
public create(): T.Static<T> {
return Value.Create(this.#schema, [])
}
/** Sets a default value for this type */
public default(value: T.Static<T>): this {
return new Type({ ...this.#schema, default: value }) as this
}
/** Parses the given value and returns the valid if valid. Otherwise throw. */
public parse(value: unknown): T.Static<T> {
this.assert(value)
return value
}
/** Compiles this type */
public compile(): this {
const compiled = new Type(this.#schema)
compiled.#assert = new TypeAssertCompiled(this.#schema)
return compiled as this
}
/** Returns the schema associated with this type */
public schema(): T {
return Value.Clone(this.#schema)
}
}
// -----------------------------------------------------------------------------------------
// Array
// -----------------------------------------------------------------------------------------
export class ArrayType<T extends T.TArray<T.TSchema>> extends Type<T> {
public maxItems(n: number) {
return this.options({ maxItems: n })
}
public minItems(n: number) {
return this.options({ minItems: n })
}
public length(n: number) {
return this.options({ minItems: n, maxItems: n })
}
public uniqueItems() {
return this.options({ uniqueItems: true })
}
}
// -----------------------------------------------------------------------------------------
// Object
// -----------------------------------------------------------------------------------------
export class ObjectType<T extends T.TObject = T.TObject> extends Type<T> {
public strict() {
return this.options({ additionalProperties: false })
}
}
// -----------------------------------------------------------------------------------------
// String
// -----------------------------------------------------------------------------------------
export class StringType extends Type<T.TString> {
public minLength(n: number) {
return this.options({ minLength: n })
}
public maxLength(n: number) {
return this.options({ maxLength: n })
}
public length(n: number) {
return this.options({ maxLength: n, minLength: n })
}
public email() {
return this.options({ format: 'email' })
}
public uuid() {
return this.options({ format: 'uuid' })
}
public url() {
return this.options({ format: 'url' })
}
public ipv6() {
return this.options({ format: 'ipv6' })
}
public ipv4() {
return this.options({ format: 'ipv4' })
}
}
// -----------------------------------------------------------------------------------------
// Number
// -----------------------------------------------------------------------------------------
export class NumberType extends Type<T.TNumber> {
public exclusiveMinimum(n: number) {
return this.options({ exclusiveMinimum: n })
}
public minimum(n: number) {
return this.options({ minimum: n })
}
public exclusiveMaximum(n: number) {
return this.options({ exclusiveMaximum: n })
}
public maximum(n: number) {
return this.options({ maximum: n })
}
public multipleOf(n: number) {
return this.options({ multipleOf: n })
}
public positive() {
return this.options({ minimum: 0 })
}
public negative() {
return this.options({ maximum: 0 })
}
}
// -----------------------------------------------------------------------------------------
// Integer
// -----------------------------------------------------------------------------------------
export class IntegerType extends Type<T.TInteger> {
public exclusiveMinimum(n: number) {
return this.options({ exclusiveMinimum: n })
}
public minimum(n: number) {
return this.options({ minimum: n })
}
public exclusiveMaximum(n: number) {
return this.options({ exclusiveMaximum: n })
}
public maximum(n: number) {
return this.options({ maximum: n })
}
public multipleOf(n: number) {
return this.options({ multipleOf: n })
}
public positive() {
return this.options({ minimum: 0 })
}
public negative() {
return this.options({ maximum: 0 })
}
}
// -----------------------------------------------------------------------------------------
// BigInt
// -----------------------------------------------------------------------------------------
export class BigIntType extends Type<T.TBigInt> {
public exclusiveMinimum(n: bigint) {
return this.options({ exclusiveMinimum: n })
}
public minimum(n: bigint) {
return this.options({ minimum: n })
}
public exclusiveMaximum(n: bigint) {
return this.options({ exclusiveMaximum: n })
}
public maximum(n: bigint) {
return this.options({ maximum: n })
}
public multipleOf(n: bigint) {
return this.options({ multipleOf: n })
}
public positive() {
return this.options({ minimum: BigInt(0) })
}
public negative() {
return this.options({ maximum: BigInt(0) })
}
}
// -----------------------------------------------------------------------------------------
// Uint8Array
// -----------------------------------------------------------------------------------------
export class ModelUint8Array<T extends T.TUint8Array> extends Type<T> {
public minByteLength(n: number) {
return this.options({ minByteLength: n })
}
public maxByteLength(n: number) {
return this.options({ maxByteLength: n })
}
}
// -----------------------------------------------------------------------------------------
// Record
// -----------------------------------------------------------------------------------------
export class RecordType<T extends T.TSchema> extends Type<T.TRecord<T.TString, T>> {}
// -----------------------------------------------------------------------------------------
// Recursive
// -----------------------------------------------------------------------------------------
export class ThisType extends Type<T.TThis> {}
export class RecursiveType<T extends T.TSchema = T.TSchema> extends Type<T> {}
// -----------------------------------------------------------------------------------------
// ModelBuilder
// -----------------------------------------------------------------------------------------
export class ModelBuilder {
/** Creates an any type */
public any() {
return new Type(T.Type.Any())
}
/** Creates an array type */
public array<T extends Type>(type: T) {
return new ArrayType(T.Type.Array(type.schema()))
}
/** Creates boolean type */
public boolean() {
return new Type(T.Type.Boolean())
}
/** Creates a bigint type */
public bigint() {
return new BigIntType(T.Type.BigInt())
}
/** Creates a date type */
public date() {
return new Type(T.Type.Date())
}
/** Creates a integer type */
public integer() {
return new IntegerType(T.Type.Integer())
}
/** Creates a number type */
public number() {
return new NumberType(T.Type.Number())
}
/** Creates a intersect type */
public intersect<T extends Type[]>(types: [...T]): Type<T.TIntersect<TypeToTuple<T>>> {
const internal = types.map((type) => type.schema())
return new Type(T.Type.Intersect(internal)) as any
}
/** Creates an keyof type */
public keyof<T extends Type>(type: T): Type<T.TKeyOf<TypeToType<T>>> {
return new Type(T.Type.KeyOf(type.schema())) as any
}
/** Creates a literal type */
public literal<T extends T.TLiteralValue>(value: T) {
return new Type(T.Type.Literal(value))
}
/** Creates a never type */
public never() {
return new Type(T.Type.Never())
}
/** Creates a null type */
public null() {
return new Type(T.Type.Null())
}
/** Creates a object type */
public object<T extends PropertiesType>(properties: T): Type<T.TObject<TypeToProperties<T>>> {
const mapped = Object.keys(properties).reduce((acc, key) => ({ ...acc, [key]: properties[key].schema() }), {} as T.TProperties)
return new ObjectType(T.Type.Object(mapped)) as any
}
/** Creates a partial type */
public partial<T extends Type>(type: T): Type<TypeToType<T>> {
return new Type(T.Type.Partial(type.schema())) as any
}
/** Creates a promise type */
public promise<T extends Type>(type: T): Type<TypeToType<T>> {
return new Type(T.Type.Promise(type.schema())) as any
}
/** Creates an unknown type */
public unknown() {
return new Type(T.Type.Unknown())
}
/** Creates a record type */
public record<T extends Type>(type: T): RecordType<TypeToType<T>> {
return new Type(T.Type.Record(T.Type.String(), type.schema())) as any
}
/** Creates a recursive type */
public recursive<T extends T.TSchema>(callback: (This: ThisType) => Type<T>) {
// prettier-ignore
return new Type(T.Type.Recursive((This) => callback(new ThisType(This)).schema()))
}
/** Creates a required type */
public required<T extends Type>(type: T) {
return new Type(T.Type.Required(type.schema()))
}
/** Creates a string type */
public string() {
return new StringType(T.Type.String())
}
/** Creates a symbol type */
public symbol() {
return new Type(T.Type.Symbol())
}
/** Creates a template literal type */
public templateLiteral<T extends TemplateLiteralType[]>(types: [...T]): Type<T.TTemplateLiteral<TypeToTuple<T>>> {
const mapped = types.map((type) => type.schema())
return new Type(T.Type.TemplateLiteral(mapped as any))
}
/** Creates a tuple type */
public tuple<T extends Type[]>(types: [...T]): Type<T.TTuple<TypeToTuple<T>>> {
return new Type(T.Type.Tuple(types.map((type) => type.schema()))) as any
}
/** Creates a uint8array type */
public uint8array() {
return new Type(T.Type.Uint8Array())
}
/** Creates an undefined type */
public undefined() {
return new Type(T.Type.Undefined())
}
/** Creates a union type */
public union<T extends Type[]>(union: [...T]): Type<T.TUnion<TypeToTuple<T>>> {
const mapped = union.map((type) => type.schema())
return new Type(T.Type.Union(mapped)) as any
}
}
export type Infer<T extends Type> = T extends Type<infer S> ? T.Static<S> : unknown
export const TypeBuilder = new ModelBuilder()
export default TypeBuilder

View File

@@ -5,59 +5,59 @@ import { readFileSync } from 'fs'
// Clean
// -------------------------------------------------------------------------------
export async function clean() {
await folder('target').delete()
await folder('target').delete()
}
// -------------------------------------------------------------------------------
// Format
// -------------------------------------------------------------------------------
export async function format() {
await shell('prettier --no-semi --single-quote --print-width 240 --trailing-comma all --write src test example/index.ts benchmark')
await shell('prettier --no-semi --single-quote --print-width 240 --trailing-comma all --write src test example/index.ts benchmark')
}
// -------------------------------------------------------------------------------
// Start
// -------------------------------------------------------------------------------
export async function start(example = 'index') {
await shell(`hammer run example/${example}.ts --dist target/example/${example}`)
await shell(`hammer run example/${example}.ts --dist target/example/${example}`)
}
// -------------------------------------------------------------------------------
// Benchmark
// -------------------------------------------------------------------------------
export async function benchmark() {
await compression()
await measurement()
await compression()
await measurement()
}
// -------------------------------------------------------------------------------
// Test
// -------------------------------------------------------------------------------
export async function test_static() {
await shell(`tsc -p test/static/tsconfig.json --noEmit --strict`)
await shell(`tsc -p test/static/tsconfig.json --noEmit --strict`)
}
export async function test_runtime(filter = '') {
await shell(`hammer build ./test/runtime/index.ts --dist target/test/runtime --platform node`)
await shell(`mocha target/test/runtime/index.js -g "${filter}"`)
await shell(`hammer build ./test/runtime/index.ts --dist target/test/runtime --platform node`)
await shell(`mocha target/test/runtime/index.js -g "${filter}"`)
}
export async function test(filter = '') {
await test_static()
await test_runtime(filter)
await test_static()
await test_runtime(filter)
}
// -------------------------------------------------------------------------------
// Build
// -------------------------------------------------------------------------------
export async function build(target = 'target/build') {
await test()
await folder(target).delete()
await shell(`tsc -p ./src/tsconfig.json --outDir ${target}`)
await folder(target).add('package.json')
await folder(target).add('readme.md')
await folder(target).add('license')
await shell(`cd ${target} && npm pack`)
await test()
await folder(target).delete()
await shell(`tsc -p ./src/tsconfig.json --outDir ${target}`)
await folder(target).add('package.json')
await folder(target).add('readme.md')
await folder(target).add('license')
await shell(`cd ${target} && npm pack`)
}
// -------------------------------------------------------------
// Publish
// -------------------------------------------------------------
export async function publish(otp, target = 'target/build') {
const { version } = JSON.parse(readFileSync('package.json', 'utf8'))
await shell(`cd ${target} && npm publish sinclair-typebox-${version}.tgz --access=public --otp ${otp}`)
await shell(`git tag ${version}`)
await shell(`git push origin ${version}`)
const { version } = JSON.parse(readFileSync('package.json', 'utf8'))
await shell(`cd ${target} && npm publish sinclair-typebox-${version}.tgz --access=public --otp ${otp}`)
await shell(`git tag ${version}`)
await shell(`git push origin ${version}`)
}