This commit is contained in:
sinclair
2025-12-24 15:44:34 +09:00
commit 13d553220c
1047 changed files with 80931 additions and 0 deletions

29
example/typedef/index.ts Normal file
View File

@@ -0,0 +1,29 @@
/*--------------------------------------------------------------------------
@sinclair/typebox/typedef
The MIT License (MIT)
Copyright (c) 2017-2025 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.
---------------------------------------------------------------------------*/
export * from './typedef'

266
example/typedef/readme.md Normal file
View File

@@ -0,0 +1,266 @@
# TypeDef
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
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'
const T = Type.Struct({
x: Type.Float32(),
y: Type.Float32(),
z: Type.Float32()
})
const V = {
x: 1,
y: 2,
z: 3
}
const R1 = TypeCompiler.Compile(T).Check(V) // true
const R2 = Value.Check(T, V) // true
```

619
example/typedef/typedef.ts Normal file
View File

@@ -0,0 +1,619 @@
/*--------------------------------------------------------------------------
@sinclair/typebox/typedef
The MIT License (MIT)
Copyright (c) 2017-2025 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 { SetErrorFunction, DefaultErrorFunction } from '@sinclair/typebox/errors'
import * as Types from '@sinclair/typebox/type'
// --------------------------------------------------------------------------
// Metadata
// --------------------------------------------------------------------------
export interface Metadata {
[name: string]: any
}
// --------------------------------------------------------------------------
// TArray
// --------------------------------------------------------------------------
export interface TArray<T extends Types.TSchema = Types.TSchema> extends Types.TSchema {
[Types.Kind]: 'TypeDef:Array'
static: Types.Static<T, this['params']>[]
elements: T
}
// --------------------------------------------------------------------------
// TBoolean
// --------------------------------------------------------------------------
export interface TBoolean extends Types.TSchema {
[Types.Kind]: 'TypeDef:Boolean'
static: 'boolean'
type: 'boolean'
}
// --------------------------------------------------------------------------
// TUnion
// --------------------------------------------------------------------------
export type InferUnion<T extends TStruct[], D extends string, Index = string> =
T extends [infer L extends TStruct, ...infer R extends TStruct[]]
? Types.Evaluate<{ [_ in D]: Index } & Types.Static<L>> | InferUnion<R, D, Types.TIncrement<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 {
[Types.Kind]: 'TypeDef:Enum'
static: T[number]
enum: [...T]
}
// --------------------------------------------------------------------------
// TFloat32
// --------------------------------------------------------------------------
export interface TFloat32 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Float32'
type: 'float32'
static: number
}
// --------------------------------------------------------------------------
// TFloat64
// --------------------------------------------------------------------------
export interface TFloat64 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Float64'
type: 'float64'
static: number
}
// --------------------------------------------------------------------------
// TInt8
// --------------------------------------------------------------------------
export interface TInt8 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Int8'
type: 'int8'
static: number
}
// --------------------------------------------------------------------------
// TInt16
// --------------------------------------------------------------------------
export interface TInt16 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Int16'
type: 'int16'
static: number
}
// --------------------------------------------------------------------------
// TInt32
// --------------------------------------------------------------------------
export interface TInt32 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Int32'
type: 'int32'
static: number
}
// --------------------------------------------------------------------------
// TUint8
// --------------------------------------------------------------------------
export interface TUint8 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Uint8'
type: 'uint8'
static: number
}
// --------------------------------------------------------------------------
// TUint16
// --------------------------------------------------------------------------
export interface TUint16 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Uint16'
type: 'uint16'
static: number
}
// --------------------------------------------------------------------------
// TUint32
// --------------------------------------------------------------------------
export interface TUint32 extends Types.TSchema {
[Types.Kind]: 'TypeDef:Uint32'
type: 'uint32'
static: number
}
// --------------------------------------------------------------------------
// TProperties
// --------------------------------------------------------------------------
export type TFields = Record<string, Types.TSchema>
// --------------------------------------------------------------------------
// TRecord
// --------------------------------------------------------------------------
export interface TRecord<T extends Types.TSchema = Types.TSchema> extends Types.TSchema {
[Types.Kind]: 'TypeDef:Record'
static: Record<string, Types.Static<T, this['params']>>
values: T
}
// --------------------------------------------------------------------------
// TString
// --------------------------------------------------------------------------
export interface TString extends Types.TSchema {
[Types.Kind]: 'TypeDef:String'
type: 'string'
static: string
}
// --------------------------------------------------------------------------
// TStruct
// --------------------------------------------------------------------------
// used for structural type inference
type OptionalKeys<T extends TFields> = { [K in keyof T]: T[K] extends (Types.TOptional<T[K]>) ? T[K] : never }
type RequiredKeys<T extends TFields> = { [K in keyof T]: T[K] extends (Types.TOptional<T[K]>) ? never : T[K] }
// static inference
type ReadonlyOptionalPropertyKeys<T extends TFields> = { [K in keyof T]: T[K] extends Types.TReadonly<Types.TSchema> ? (T[K] extends Types.TOptional<T[K]> ? K : never) : never }[keyof T]
type ReadonlyPropertyKeys<T extends TFields> = { [K in keyof T]: T[K] extends Types.TReadonly<Types.TSchema> ? (T[K] extends Types.TOptional<T[K]> ? never : K) : never }[keyof T]
type OptionalPropertyKeys<T extends TFields> = { [K in keyof T]: T[K] extends Types.TOptional<Types.TSchema> ? (T[K] extends Types.TReadonly<T[K]> ? never : K) : never }[keyof T]
type RequiredPropertyKeys<T extends TFields> = keyof Omit<T, ReadonlyOptionalPropertyKeys<T> | ReadonlyPropertyKeys<T> | OptionalPropertyKeys<T>>
// prettier-ignore
type StructStaticProperties<T extends TFields, R extends Record<keyof any, unknown>> = Types.Evaluate<(
Readonly<Partial<Pick<R, ReadonlyOptionalPropertyKeys<T>>>> &
Readonly<Pick<R, ReadonlyPropertyKeys<T>>> &
Partial<Pick<R, OptionalPropertyKeys<T>>> &
Required<Pick<R, RequiredPropertyKeys<T>>>
)>
// prettier-ignore
export type StructStatic<T extends TFields, P extends unknown[]> = StructStaticProperties<T, {
[K in keyof T]: Static<T[K], P>
}>
export interface StructMetadata extends Metadata {
additionalProperties?: boolean
}
export interface TStruct<T extends TFields = TFields> extends Types.TSchema, StructMetadata {
[Types.Kind]: 'TypeDef:Struct'
static: StructStatic<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] }
}
// --------------------------------------------------------------------------
// TTimestamp
// --------------------------------------------------------------------------
export interface TTimestamp extends Types.TSchema {
[Types.Kind]: 'TypeDef:Timestamp'
type: 'timestamp'
static: string
}
// --------------------------------------------------------------------------
// Static
// --------------------------------------------------------------------------
export type Static<T extends Types.TSchema, P extends unknown[] = []> = Types.Static<T, P>
// --------------------------------------------------------------------------
// TimestampFormat
// --------------------------------------------------------------------------
export namespace TimestampFormat {
const DATE_TIME_SEPARATOR = /t|\s/i
const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i
const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
function IsLeapYear(year: number): boolean {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
}
function IsDate(str: string): boolean {
const matches: string[] | null = DATE.exec(str)
if (!matches) return false
const year: number = +matches[1]
const month: number = +matches[2]
const day: number = +matches[3]
return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && IsLeapYear(year) ? 29 : DAYS[month])
}
function IsTime(str: string, strictTimeZone?: boolean): boolean {
const matches: string[] | null = TIME.exec(str)
if (!matches) return false
const hr: number = +matches[1]
const min: number = +matches[2]
const sec: number = +matches[3]
const tz: string | undefined = matches[4]
const tzSign: number = matches[5] === '-' ? -1 : 1
const tzH: number = +(matches[6] || 0)
const tzM: number = +(matches[7] || 0)
if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz)) return false
if (hr <= 23 && min <= 59 && sec < 60) return true
const utcMin = min - tzM * tzSign
const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0)
return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61
}
function IsDateTime(value: string, strictTimeZone?: boolean): boolean {
const dateTime: string[] = value.split(DATE_TIME_SEPARATOR)
return dateTime.length === 2 && IsDate(dateTime[0]) && IsTime(dateTime[1], strictTimeZone)
}
export function Check(value: string): boolean {
return IsDateTime(value)
}
}
// --------------------------------------------------------------------------
// ValueCheck
// --------------------------------------------------------------------------
export class ValueCheckError extends Types.TypeBoxError {
constructor(public readonly schema: Types.TSchema) {
super('Unknown type')
}
}
export namespace ValueCheck {
// ------------------------------------------------------------------------
// Guards
// ------------------------------------------------------------------------
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[] {
return globalThis.Array.isArray(value)
}
function IsString(value: unknown): value is string {
return typeof value === 'string'
}
function IsInt(value: unknown, min: number, max: number): value is number {
return typeof value === 'number' && globalThis.Number.isInteger(value) && value >= min && value < max
}
// ------------------------------------------------------------------------
// Types
// ------------------------------------------------------------------------
function Array(schema: TArray, value: unknown): boolean {
return IsArray(value) && value.every(value => Visit(schema.elements, value))
}
function Boolean(schema: TBoolean, value: unknown): boolean {
return typeof value === 'boolean'
}
function Enum(schema: TEnum, value: unknown): boolean {
return typeof value === 'string' && schema.enum.includes(value)
}
function Float32(schema: TFloat32, value: unknown): boolean {
return typeof value === 'number'
}
function Float64(schema: TFloat64, value: unknown): boolean {
return typeof value === 'number'
}
function Int8(schema: TInt8, value: unknown): boolean {
return IsInt(value, -128, 127)
}
function Int16(schema: TInt16, value: unknown): boolean {
return IsInt(value, -32_768, 32_767)
}
function Int32(schema: TInt32, value: unknown): boolean {
return IsInt(value, -2_147_483_648, 2_147_483_647)
}
function Uint8(schema: TUint8, value: unknown): boolean {
return IsInt(value, 0, 255)
}
function Uint16(schema: TUint16, value: unknown): boolean {
return IsInt(value, 0, 65535)
}
function Uint32(schema: TUint32, value: unknown): boolean {
return IsInt(value, 0, 4_294_967_295)
}
function Record(schema: TRecord, value: unknown): boolean {
return IsObject(value) && globalThis.Object.getOwnPropertyNames(value).every(key => Visit(schema.values, value[key]))
}
function String(schema: TString, value: unknown): boolean {
return typeof value === 'string'
}
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)
const unknownKeys = globalThis.Object.getOwnPropertyNames(value)
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 IsString(value) && TimestampFormat.Check(value)
}
function Union(schema: TUnion, value: unknown): boolean {
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]) {
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)
case 'TypeDef:Int8': return Int8(anySchema, value)
case 'TypeDef:Int16': return Int16(anySchema, value)
case 'TypeDef:Int32': return Int32(anySchema, value)
case 'TypeDef:Uint8': return Uint8(anySchema, value)
case 'TypeDef:Uint16': return Uint16(anySchema, value)
case 'TypeDef:Uint32': return Uint32(anySchema, value)
case 'TypeDef:Record': return Record(anySchema, value)
case 'TypeDef:String': return String(anySchema, value)
case 'TypeDef:Struct': return Struct(anySchema, value)
case 'TypeDef:Timestamp': return Timestamp(anySchema, value)
default: throw new ValueCheckError(anySchema)
}
}
export function Check<T extends Types.TSchema>(schema: T, value: unknown): value is Types.Static<T> {
return Visit(schema, value)
}
}
// --------------------------------------------------------------------------
// TypeGuard
// --------------------------------------------------------------------------
export namespace TypeGuard {
// ------------------------------------------------------------------------
// Guards
// ------------------------------------------------------------------------
function IsObject(value: unknown): value is Record<keyof any, unknown> {
return typeof value === 'object'
}
function IsArray(value: unknown): value is unknown[] {
return globalThis.Array.isArray(value)
}
function IsOptionalBoolean(value: unknown): value is boolean | undefined {
return IsBoolean(value) || value === undefined
}
function IsBoolean(value: unknown): value is boolean {
return typeof value === 'boolean'
}
function IsString(value: unknown): value is string {
return typeof value === 'string'
}
// ------------------------------------------------------------------------
// Types
// ------------------------------------------------------------------------
export function TArray(schema: unknown): schema is TArray {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Array' && TSchema(schema['elements'])
}
export function TBoolean(schema: unknown): schema is TBoolean {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Boolean' && schema['type'] === 'boolean'
}
export function TUnion(schema: unknown): schema is TUnion {
if(!(IsObject(schema) && schema[Types.Kind] === 'TypeDef:Union' && IsString(schema['discriminator']) && IsObject(schema['mapping']))) return false
return globalThis.Object.getOwnPropertyNames(schema['mapping']).every(key => TSchema((schema['mapping'] as any)[key]))
}
export function TEnum(schema: unknown): schema is TEnum {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Enum' && IsArray(schema['enum']) && schema['enum'].every(item => IsString(item))
}
export function TFloat32(schema: unknown): schema is TFloat32 {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Float32' && schema['type'] === 'float32'
}
export function TFloat64(schema: unknown): schema is TFloat64 {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Float64' && schema['type'] === 'float64'
}
export function TInt8(schema: unknown): schema is TInt8 {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Int8' && schema['type'] === 'int8'
}
export function TInt16(schema: unknown): schema is TInt16 {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Int16' && schema['type'] === 'int16'
}
export function TInt32(schema: unknown): schema is TInt32 {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Int32' && schema['type'] === 'int32'
}
export function TUint8(schema: unknown): schema is TUint8 {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Uint8' && schema['type'] === 'uint8'
}
export function TUint16(schema: unknown): schema is TUint16 {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Uint16' && schema['type'] === 'uint16'
}
export function TUint32(schema: unknown): schema is TUint32 {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Uint32' && schema['type'] === 'uint32'
}
export function TRecord(schema: unknown): schema is TRecord {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Record' && TSchema(schema['values'])
}
export function TString(schema: unknown): schema is TString {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:String' && schema['type'] === 'string'
}
export function TStruct(schema: unknown): schema is TStruct {
if(!(IsObject(schema) && schema[Types.Kind] === 'TypeDef:Struct' && IsOptionalBoolean(schema['additionalProperties']))) return false
const optionalProperties = schema['optionalProperties']
const requiredProperties = schema['properties']
const optionalCheck = optionalProperties === undefined || IsObject(optionalProperties) && globalThis.Object.getOwnPropertyNames(optionalProperties).every(key => TSchema(optionalProperties[key]))
const requiredCheck = requiredProperties === undefined || IsObject(requiredProperties) && globalThis.Object.getOwnPropertyNames(requiredProperties).every(key => TSchema(requiredProperties[key]))
return optionalCheck && requiredCheck
}
export function TTimestamp(schema: unknown): schema is TTimestamp {
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Timestamp' && schema['type'] === 'timestamp'
}
export function TKind(schema: unknown): schema is Types.TKind {
return IsObject(schema) && Types.Kind in schema && typeof (schema as any)[Types.Kind] === 'string' // TS 4.1.5: any required for symbol indexer
}
export function TSchema(schema: unknown): schema is Types.TSchema {
// prettier-ignore
return (
TArray(schema) ||
TBoolean(schema) ||
TUnion(schema) ||
TEnum(schema) ||
TFloat32(schema) ||
TFloat64(schema) ||
TInt8(schema) ||
TInt16(schema) ||
TInt32(schema) ||
TUint8(schema) ||
TUint16(schema) ||
TUint32(schema) ||
TRecord(schema) ||
TString(schema) ||
TStruct(schema) ||
TTimestamp(schema) ||
(TKind(schema) && Types.TypeRegistry.Has(schema[Types.Kind]))
)
}
}
// --------------------------------------------------------------------------
// TypeRegistry
// --------------------------------------------------------------------------
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))
// --------------------------------------------------------------------------
// TypeSystemErrorFunction
// --------------------------------------------------------------------------
SetErrorFunction((error) => {
switch(error.schema[Types.Kind]) {
case 'TypeDef:Array': return 'Expected Array'
case 'TypeDef:Boolean': return 'Expected Boolean'
case 'TypeDef:Union': return 'Expected Union'
case 'TypeDef:Int8': return 'Expected Int8'
case 'TypeDef:Int16': return 'Expected Int16'
case 'TypeDef:Int32': return 'Expected Int32'
case 'TypeDef:Uint8': return 'Expected Uint8'
case 'TypeDef:Uint16': return 'Expected Uint16'
case 'TypeDef:Uint32': return 'Expected Uint32'
case 'TypeDef:Record': return 'Expected Record'
case 'TypeDef:String': return 'Expected String'
case 'TypeDef:Struct': return 'Expected Struct'
case 'TypeDef:Timestamp': return 'Expected Timestamp'
}
return DefaultErrorFunction(error)
})
// --------------------------------------------------------------------------
// TypeDefBuilder
// --------------------------------------------------------------------------
export class TypeDefBuilder {
// ------------------------------------------------------------------------
// Core
// ------------------------------------------------------------------------
protected Create(schema: Record<PropertyKey, any>, metadata: Record<keyof any, any>): any {
const keys = globalThis.Object.getOwnPropertyNames(metadata)
return keys.length > 0 ? { ...schema, metadata: { ...metadata } } : { ...schema }
}
/** [Standard] Removes compositing symbols from this schema */
public Strict<T extends Types.TSchema>(schema: T): T {
return JSON.parse(JSON.stringify(schema)) as T
}
// ------------------------------------------------------------------------
// Modifiers
// ------------------------------------------------------------------------
/** `[Standard]` Creates an Optional property */
public Optional<T extends Types.TSchema>(schema: T): Types.TOptional<T> {
return this.Optional(schema)
}
/** `[Standard]` Creates a Readonly property */
public Readonly<T extends Types.TSchema>(schema: T): Types.TReadonly<T> {
return this.Readonly(schema)
}
// ------------------------------------------------------------------------
// Types
// ------------------------------------------------------------------------
/** [Standard] Creates a Array type */
public Array<T extends Types.TSchema>(elements: T, metadata: Metadata = {}): TArray<T> {
return this.Create({ [Types.Kind]: 'TypeDef:Array', elements }, metadata)
}
/** [Standard] Creates a Boolean type */
public Boolean(metadata: Metadata = {}): TBoolean {
return this.Create({ [Types.Kind]: 'TypeDef:Boolean', type: 'boolean' }, metadata)
}
/** [Standard] Creates a Enum type */
public Enum<T extends string[]>(values: [...T], metadata: Metadata = {}): TEnum<T> {
return this.Create({[Types.Kind]: 'TypeDef:Enum', enum: values }, metadata )
}
/** [Standard] Creates a Float32 type */
public Float32(metadata: Metadata = {}): TFloat32 {
return this.Create({ [Types.Kind]: 'TypeDef:Float32', type: 'float32' }, metadata)
}
/** [Standard] Creates a Float64 type */
public Float64(metadata: Metadata = {}): TFloat64 {
return this.Create({ [Types.Kind]: 'TypeDef:Float64', type: 'float64' }, metadata)
}
/** [Standard] Creates a Int8 type */
public Int8(metadata: Metadata = {}): TInt8 {
return this.Create({ [Types.Kind]: 'TypeDef:Int8', type: 'int8' }, metadata)
}
/** [Standard] Creates a Int16 type */
public Int16(metadata: Metadata = {}): TInt16 {
return this.Create({ [Types.Kind]: 'TypeDef:Int16', type: 'int16' }, metadata)
}
/** [Standard] Creates a Int32 type */
public Int32(metadata: Metadata = {}): TInt32 {
return this.Create({ [Types.Kind]: 'TypeDef:Int32', type: 'int32' }, metadata)
}
/** [Standard] Creates a Uint8 type */
public Uint8(metadata: Metadata = {}): TUint8 {
return this.Create({ [Types.Kind]: 'TypeDef:Uint8', type: 'uint8' }, metadata)
}
/** [Standard] Creates a Uint16 type */
public Uint16(metadata: Metadata = {}): TUint16 {
return this.Create({ [Types.Kind]: 'TypeDef:Uint16', type: 'uint16' }, metadata)
}
/** [Standard] Creates a Uint32 type */
public Uint32(metadata: Metadata = {}): TUint32 {
return this.Create({ [Types.Kind]: 'TypeDef:Uint32', type: 'uint32' }, metadata)
}
/** [Standard] Creates a Record type */
public Record<T extends Types.TSchema>(values: T, metadata: Metadata = {}): TRecord<T> {
return this.Create({ [Types.Kind]: 'TypeDef:Record', values },metadata)
}
/** [Standard] Creates a String type */
public String(metadata: Metadata = {}): TString {
return this.Create({ [Types.Kind]: 'TypeDef:String', type: 'string' }, metadata)
}
/** [Standard] Creates a Struct type */
public Struct<T extends TFields>(fields: T, metadata: StructMetadata = {}): TStruct<T> {
const optionalProperties = globalThis.Object.getOwnPropertyNames(fields).reduce((acc, key) => (Types.TypeGuard.IsOptional(fields[key]) ? { ...acc, [key]: fields[key] } : { ...acc }), {} as TFields)
const properties = globalThis.Object.getOwnPropertyNames(fields).reduce((acc, key) => (Types.TypeGuard.IsOptional(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({ [Types.Kind]: 'TypeDef:Struct', ...requiredObject, ...optionalObject }, metadata)
}
/** [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('TypeDefBuilder: 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(metadata: Metadata = {}): TTimestamp {
return this.Create({ [Types.Kind]: 'TypeDef:Timestamp', type: 'timestamp' }, metadata)
}
}
/** JSON Type Definition Type Builder */
export const Type = new TypeDefBuilder()