Skip to content

Commit da49443

Browse files
authored
TypeGuards for TypeDef (#504)
1 parent 2bf9244 commit da49443

File tree

1 file changed

+204
-49
lines changed

1 file changed

+204
-49
lines changed

example/typedef/typedef.ts

Lines changed: 204 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ THE SOFTWARE.
2626
2727
---------------------------------------------------------------------------*/
2828

29-
export { Static, Evaluate, TSchema, PropertiesReduce, TReadonly, TReadonlyOptional, TOptional } from '@sinclair/typebox'
3029
import * as Types from '@sinclair/typebox'
3130

3231
// --------------------------------------------------------------------------
@@ -41,6 +40,12 @@ export type Tick<T extends string, B extends Base> = T extends keyof B ? B[T] :
4140
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
4241
export type Increment<T extends string, B extends Base = Base10> = Reverse<Next<Reverse<T>, B>>
4342
// --------------------------------------------------------------------------
43+
// SchemaOptions
44+
// --------------------------------------------------------------------------
45+
export interface SchemaOptions {
46+
[name: string]: any
47+
}
48+
// --------------------------------------------------------------------------
4449
// TArray
4550
// --------------------------------------------------------------------------
4651
export interface TArray<T extends Types.TSchema = Types.TSchema> extends Types.TSchema {
@@ -158,14 +163,15 @@ export interface TRecord<T extends Types.TSchema = Types.TSchema> extends Types.
158163
// --------------------------------------------------------------------------
159164
export interface TString extends Types.TSchema {
160165
[Types.Kind]: 'TypeDef:String'
166+
type: 'string'
161167
static: string
162168
}
163169
// --------------------------------------------------------------------------
164170
// TStruct
165171
// --------------------------------------------------------------------------
166172
type OptionalKeys<T extends TFields> = { [K in keyof T]: T[K] extends (Types.TReadonlyOptional<T[K]> | Types.TOptional<T[K]>) ? T[K] : never }
167173
type RequiredKeys<T extends TFields> = { [K in keyof T]: T[K] extends (Types.TReadonlyOptional<T[K]> | Types.TOptional<T[K]>) ? never : T[K] }
168-
export interface StructOptions {
174+
export interface StructOptions extends SchemaOptions {
169175
additionalProperties?: boolean
170176
}
171177
export interface TStruct<T extends TFields = TFields> extends Types.TSchema, StructOptions {
@@ -179,24 +185,52 @@ export interface TStruct<T extends TFields = TFields> extends Types.TSchema, Str
179185
// --------------------------------------------------------------------------
180186
export interface TTimestamp extends Types.TSchema {
181187
[Types.Kind]: 'TypeDef:Timestamp'
182-
static: number
188+
type: 'timestamp'
189+
static: string
183190
}
184191
// --------------------------------------------------------------------------
185-
// TypeRegistry
186-
// --------------------------------------------------------------------------
187-
Types.TypeRegistry.Set<TArray>('TypeDef:Array', (schema, value) => ValueCheck.Check(schema, value))
188-
Types.TypeRegistry.Set<TBoolean>('TypeDef:Boolean', (schema, value) => ValueCheck.Check(schema, value))
189-
Types.TypeRegistry.Set<TUnion>('TypeDef:Union', (schema, value) => ValueCheck.Check(schema, value))
190-
Types.TypeRegistry.Set<TInt8>('TypeDef:Int8', (schema, value) => ValueCheck.Check(schema, value))
191-
Types.TypeRegistry.Set<TInt16>('TypeDef:Int16', (schema, value) => ValueCheck.Check(schema, value))
192-
Types.TypeRegistry.Set<TInt32>('TypeDef:Int32', (schema, value) => ValueCheck.Check(schema, value))
193-
Types.TypeRegistry.Set<TUint8>('TypeDef:Uint8', (schema, value) => ValueCheck.Check(schema, value))
194-
Types.TypeRegistry.Set<TUint16>('TypeDef:Uint16', (schema, value) => ValueCheck.Check(schema, value))
195-
Types.TypeRegistry.Set<TUint32>('TypeDef:Uint32', (schema, value) => ValueCheck.Check(schema, value))
196-
Types.TypeRegistry.Set<TRecord>('TypeDef:Record', (schema, value) => ValueCheck.Check(schema, value))
197-
Types.TypeRegistry.Set<TString>('TypeDef:String', (schema, value) => ValueCheck.Check(schema, value))
198-
Types.TypeRegistry.Set<TStruct>('TypeDef:Struct', (schema, value) => ValueCheck.Check(schema, value))
199-
Types.TypeRegistry.Set<TTimestamp>('TypeDef:Timestamp', (schema, value) => ValueCheck.Check(schema, value))
192+
// TimestampFormat
193+
// --------------------------------------------------------------------------
194+
export namespace TimestampFormat {
195+
const DATE_TIME_SEPARATOR = /t|\s/i
196+
const TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i
197+
const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/
198+
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
199+
function IsLeapYear(year: number): boolean {
200+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
201+
}
202+
function IsDate(str: string): boolean {
203+
const matches: string[] | null = DATE.exec(str)
204+
if (!matches) return false
205+
const year: number = +matches[1]
206+
const month: number = +matches[2]
207+
const day: number = +matches[3]
208+
return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && IsLeapYear(year) ? 29 : DAYS[month])
209+
}
210+
function IsTime(str: string, strictTimeZone?: boolean): boolean {
211+
const matches: string[] | null = TIME.exec(str)
212+
if (!matches) return false
213+
const hr: number = +matches[1]
214+
const min: number = +matches[2]
215+
const sec: number = +matches[3]
216+
const tz: string | undefined = matches[4]
217+
const tzSign: number = matches[5] === '-' ? -1 : 1
218+
const tzH: number = +(matches[6] || 0)
219+
const tzM: number = +(matches[7] || 0)
220+
if (tzH > 23 || tzM > 59 || (strictTimeZone && !tz)) return false
221+
if (hr <= 23 && min <= 59 && sec < 60) return true
222+
const utcMin = min - tzM * tzSign
223+
const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0)
224+
return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61
225+
}
226+
function IsDateTime(value: string, strictTimeZone?: boolean): boolean {
227+
const dateTime: string[] = value.split(DATE_TIME_SEPARATOR)
228+
return dateTime.length === 2 && IsDate(dateTime[0]) && IsTime(dateTime[1], strictTimeZone)
229+
}
230+
export function Check(value: string): boolean {
231+
return IsDateTime(value)
232+
}
233+
}
200234
// --------------------------------------------------------------------------
201235
// ValueCheck
202236
// --------------------------------------------------------------------------
@@ -287,7 +321,7 @@ export namespace ValueCheck {
287321
return true
288322
}
289323
function Timestamp(schema: TString, value: unknown): boolean {
290-
return IsInt(value, 0, Number.MAX_SAFE_INTEGER)
324+
return IsString(value) && TimestampFormat.Check(value)
291325
}
292326
function Union(schema: TUnion, value: unknown): boolean {
293327
if (!IsObject(value)) return false
@@ -324,9 +358,130 @@ export namespace ValueCheck {
324358
}
325359
}
326360
// --------------------------------------------------------------------------
327-
// TypeDefTypeBuilder
361+
// TypeGuard
362+
// --------------------------------------------------------------------------
363+
export namespace TypeGuard {
364+
// ------------------------------------------------------------------------
365+
// Guards
366+
// ------------------------------------------------------------------------
367+
function IsObject(value: unknown): value is Record<keyof any, unknown> {
368+
return typeof value === 'object'
369+
}
370+
function IsArray(value: unknown): value is unknown[] {
371+
return globalThis.Array.isArray(value)
372+
}
373+
function IsOptionalBoolean(value: unknown): value is boolean | undefined {
374+
return IsBoolean(value) || value === undefined
375+
}
376+
function IsBoolean(value: unknown): value is boolean {
377+
return typeof value === 'boolean'
378+
}
379+
function IsString(value: unknown): value is string {
380+
return typeof value === 'string'
381+
}
382+
// ------------------------------------------------------------------------
383+
// Types
384+
// ------------------------------------------------------------------------
385+
export function TArray(schema: unknown): schema is TArray {
386+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Array' && TSchema(schema['elements'])
387+
}
388+
export function TBoolean(schema: unknown): schema is TBoolean {
389+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Boolean' && schema['type'] === 'boolean'
390+
}
391+
export function TUnion(schema: unknown): schema is TUnion {
392+
if(!(IsObject(schema) && schema[Types.Kind] === 'TypeDef:Union' && IsString(schema['discriminator']) && IsObject(schema['mapping']))) return false
393+
return globalThis.Object.getOwnPropertyNames(schema['mapping']).every(key => TSchema((schema['mapping'] as any)[key]))
394+
}
395+
export function TEnum(schema: unknown): schema is TEnum {
396+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Enum' && IsArray(schema['enum']) && schema['enum'].every(item => IsString(item))
397+
}
398+
export function TFloat32(schema: unknown): schema is TFloat32 {
399+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Float32' && schema['type'] === 'float32'
400+
}
401+
export function TFloat64(schema: unknown): schema is TFloat64 {
402+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Float64' && schema['type'] === 'float64'
403+
}
404+
export function TInt8(schema: unknown): schema is TInt8 {
405+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Int8' && schema['type'] === 'int8'
406+
}
407+
export function TInt16(schema: unknown): schema is TInt16 {
408+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Int16' && schema['type'] === 'int16'
409+
}
410+
export function TInt32(schema: unknown): schema is TInt32 {
411+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Int32' && schema['type'] === 'int32'
412+
}
413+
export function TUint8(schema: unknown): schema is TUint8 {
414+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Uint8' && schema['type'] === 'uint8'
415+
}
416+
export function TUint16(schema: unknown): schema is TUint16 {
417+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Uint16' && schema['type'] === 'uint16'
418+
}
419+
export function TUint32(schema: unknown): schema is TUint32 {
420+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Uint32' && schema['type'] === 'uint32'
421+
}
422+
export function TRecord(schema: unknown): schema is TRecord {
423+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Record' && TSchema(schema['values'])
424+
}
425+
export function TString(schema: unknown): schema is TString {
426+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:String' && schema['type'] === 'string'
427+
}
428+
export function TStruct(schema: unknown): schema is TStruct {
429+
if(!(IsObject(schema) && schema[Types.Kind] === 'TypeDef:Struct' && IsOptionalBoolean(schema['additionalProperties']))) return false
430+
const optionalProperties = schema['optionalProperties']
431+
const requiredProperties = schema['properties']
432+
const optionalCheck = optionalProperties === undefined || IsObject(optionalProperties) && globalThis.Object.getOwnPropertyNames(optionalProperties).every(key => TSchema(optionalProperties[key]))
433+
const requiredCheck = requiredProperties === undefined || IsObject(requiredProperties) && globalThis.Object.getOwnPropertyNames(requiredProperties).every(key => TSchema(requiredProperties[key]))
434+
return optionalCheck && requiredCheck
435+
}
436+
export function TTimestamp(schema: unknown): schema is TTimestamp {
437+
return IsObject(schema) && schema[Types.Kind] === 'TypeDef:Timestamp' && schema['type'] === 'timestamp'
438+
}
439+
export function TKind(schema: unknown): schema is Types.TKind {
440+
return IsObject(schema) && Types.Kind in schema && typeof (schema as any)[Types.Kind] === 'string' // TS 4.1.5: any required for symbol indexer
441+
}
442+
export function TSchema(schema: unknown): schema is Types.TSchema {
443+
// prettier-ignore
444+
return (
445+
TArray(schema) ||
446+
TBoolean(schema) ||
447+
TUnion(schema) ||
448+
TEnum(schema) ||
449+
TFloat32(schema) ||
450+
TFloat64(schema) ||
451+
TInt8(schema) ||
452+
TInt16(schema) ||
453+
TInt32(schema) ||
454+
TUint8(schema) ||
455+
TUint16(schema) ||
456+
TUint32(schema) ||
457+
TRecord(schema) ||
458+
TString(schema) ||
459+
TStruct(schema) ||
460+
TTimestamp(schema) ||
461+
(TKind(schema) && Types.TypeRegistry.Has(schema[Types.Kind]))
462+
)
463+
}
464+
}
465+
// --------------------------------------------------------------------------
466+
// TypeRegistry
467+
// --------------------------------------------------------------------------
468+
Types.TypeRegistry.Set<TArray>('TypeDef:Array', (schema, value) => ValueCheck.Check(schema, value))
469+
Types.TypeRegistry.Set<TBoolean>('TypeDef:Boolean', (schema, value) => ValueCheck.Check(schema, value))
470+
Types.TypeRegistry.Set<TUnion>('TypeDef:Union', (schema, value) => ValueCheck.Check(schema, value))
471+
Types.TypeRegistry.Set<TInt8>('TypeDef:Int8', (schema, value) => ValueCheck.Check(schema, value))
472+
Types.TypeRegistry.Set<TInt16>('TypeDef:Int16', (schema, value) => ValueCheck.Check(schema, value))
473+
Types.TypeRegistry.Set<TInt32>('TypeDef:Int32', (schema, value) => ValueCheck.Check(schema, value))
474+
Types.TypeRegistry.Set<TUint8>('TypeDef:Uint8', (schema, value) => ValueCheck.Check(schema, value))
475+
Types.TypeRegistry.Set<TUint16>('TypeDef:Uint16', (schema, value) => ValueCheck.Check(schema, value))
476+
Types.TypeRegistry.Set<TUint32>('TypeDef:Uint32', (schema, value) => ValueCheck.Check(schema, value))
477+
Types.TypeRegistry.Set<TRecord>('TypeDef:Record', (schema, value) => ValueCheck.Check(schema, value))
478+
Types.TypeRegistry.Set<TString>('TypeDef:String', (schema, value) => ValueCheck.Check(schema, value))
479+
Types.TypeRegistry.Set<TStruct>('TypeDef:Struct', (schema, value) => ValueCheck.Check(schema, value))
480+
Types.TypeRegistry.Set<TTimestamp>('TypeDef:Timestamp', (schema, value) => ValueCheck.Check(schema, value))
481+
// --------------------------------------------------------------------------
482+
// TypeDefBuilder
328483
// --------------------------------------------------------------------------
329-
export class TypeDefTypeBuilder extends Types.TypeBuilder {
484+
export class TypeDefBuilder extends Types.TypeBuilder {
330485
// ------------------------------------------------------------------------
331486
// Modifiers
332487
// ------------------------------------------------------------------------
@@ -346,56 +501,56 @@ export class TypeDefTypeBuilder extends Types.TypeBuilder {
346501
// Types
347502
// ------------------------------------------------------------------------
348503
/** [Standard] Creates a Array type */
349-
public Array<T extends Types.TSchema>(elements: T): TArray<T> {
350-
return this.Create({ [Types.Kind]: 'TypeDef:Array', elements })
504+
public Array<T extends Types.TSchema>(elements: T, options: SchemaOptions = {}): TArray<T> {
505+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Array', elements })
351506
}
352507
/** [Standard] Creates a Boolean type */
353-
public Boolean(): TBoolean {
354-
return this.Create({ [Types.Kind]: 'TypeDef:Boolean', type: 'boolean' })
508+
public Boolean(options: SchemaOptions = {}): TBoolean {
509+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Boolean', type: 'boolean' })
355510
}
356511
/** [Standard] Creates a Enum type */
357-
public Enum<T extends string[]>(values: [...T]): TEnum<T> {
358-
return this.Create({ [Types.Kind]: 'TypeDef:Enum', enum: values })
512+
public Enum<T extends string[]>(values: [...T], options: SchemaOptions = {}): TEnum<T> {
513+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Enum', enum: values })
359514
}
360515
/** [Standard] Creates a Float32 type */
361-
public Float32(): TFloat32 {
362-
return this.Create({ [Types.Kind]: 'TypeDef:Float32', type: 'float32' })
516+
public Float32(options: SchemaOptions = {}): TFloat32 {
517+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Float32', type: 'float32' })
363518
}
364519
/** [Standard] Creates a Float64 type */
365-
public Float64(): TFloat64 {
366-
return this.Create({ [Types.Kind]: 'TypeDef:Float64', type: 'float64' })
520+
public Float64(options: SchemaOptions = {}): TFloat64 {
521+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Float64', type: 'float64' })
367522
}
368523
/** [Standard] Creates a Int8 type */
369-
public Int8(): TInt8 {
370-
return this.Create({ [Types.Kind]: 'TypeDef:Int8', type: 'int8' })
524+
public Int8(options: SchemaOptions = {}): TInt8 {
525+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Int8', type: 'int8' })
371526
}
372527
/** [Standard] Creates a Int16 type */
373-
public Int16(): TInt16 {
374-
return this.Create({ [Types.Kind]: 'TypeDef:Int16', type: 'int16' })
528+
public Int16(options: SchemaOptions = {}): TInt16 {
529+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Int16', type: 'int16' })
375530
}
376531
/** [Standard] Creates a Int32 type */
377-
public Int32(): TInt32 {
378-
return this.Create({ [Types.Kind]: 'TypeDef:Int32', type: 'int32' })
532+
public Int32(options: SchemaOptions = {}): TInt32 {
533+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Int32', type: 'int32' })
379534
}
380535
/** [Standard] Creates a Uint8 type */
381-
public Uint8(): TUint8 {
382-
return this.Create({ [Types.Kind]: 'TypeDef:Uint8', type: 'uint8' })
536+
public Uint8(options: SchemaOptions = {}): TUint8 {
537+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Uint8', type: 'uint8' })
383538
}
384539
/** [Standard] Creates a Uint16 type */
385-
public Uint16(): TUint16 {
386-
return this.Create({ [Types.Kind]: 'TypeDef:Uint16', type: 'uint16' })
540+
public Uint16(options: SchemaOptions = {}): TUint16 {
541+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Uint16', type: 'uint16' })
387542
}
388543
/** [Standard] Creates a Uint32 type */
389-
public Uint32(): TUint32 {
390-
return this.Create({ [Types.Kind]: 'TypeDef:Uint32', type: 'uint32' })
544+
public Uint32(options: SchemaOptions = {}): TUint32 {
545+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Uint32', type: 'uint32' })
391546
}
392547
/** [Standard] Creates a Record type */
393-
public Record<T extends Types.TSchema>(values: T): TRecord<T> {
394-
return this.Create({ [Types.Kind]: 'TypeDef:Record', values })
548+
public Record<T extends Types.TSchema>(values: T, options: SchemaOptions = {}): TRecord<T> {
549+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:Record', values })
395550
}
396551
/** [Standard] Creates a String type */
397-
public String(): TString {
398-
return this.Create({ [Types.Kind]: 'TypeDef:String', type: 'string' })
552+
public String(options: SchemaOptions = {}): TString {
553+
return this.Create({ ...options, [Types.Kind]: 'TypeDef:String', type: 'string' })
399554
}
400555
/** [Standard] Creates a Struct type */
401556
public Struct<T extends TFields>(fields: T, options?: StructOptions): TStruct<T> {
@@ -408,7 +563,7 @@ export class TypeDefTypeBuilder extends Types.TypeBuilder {
408563
/** [Standard] Creates a Union type */
409564
public Union<T extends TStruct<TFields>[], D extends string = 'type'>(structs: [...T], discriminator?: D): TUnion<T, D> {
410565
discriminator = (discriminator || 'type') as D
411-
if (structs.length === 0) throw new Error('TypeDefTypeBuilder: Union types must contain at least one struct')
566+
if (structs.length === 0) throw new Error('TypeDefBuilder: Union types must contain at least one struct')
412567
const mapping = structs.reduce((acc, current, index) => ({ ...acc, [index.toString()]: current }), {})
413568
return this.Create({ [Types.Kind]: 'TypeDef:Union', discriminator, mapping })
414569
}
@@ -419,5 +574,5 @@ export class TypeDefTypeBuilder extends Types.TypeBuilder {
419574
}
420575

421576
/** JSON Type Definition Type Builder */
422-
export const Type = new TypeDefTypeBuilder()
577+
export const Type = new TypeDefBuilder()
423578

0 commit comments

Comments
 (0)