diff --git a/license.md b/license similarity index 87% rename from license.md rename to license index 9399b4a..9871968 100644 --- a/license.md +++ b/license @@ -1,5 +1,3 @@ -/*-------------------------------------------------------------------------- - TypeBox: JSONSchema Type Builder with Static Type Resolution for TypeScript The MIT License (MIT) @@ -22,6 +20,4 @@ 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. - ----------------------------------------------------------------------------*/ \ No newline at end of file +THE SOFTWARE. \ No newline at end of file diff --git a/readme.md b/readme.md index 39f0340..d2e8533 100644 --- a/readme.md +++ b/readme.md @@ -9,26 +9,25 @@ - ## Install -``` -npm install @sinclair/typebox --save +```bash +$ npm install @sinclair/typebox --save ``` ## Overview -TypeBox is a type builder library that allows developers to compose complex in-memory JSONSchema objects that can be resolved to static TypeScript types. TypeBox internally represents its types as plain JSONSchema objects and leverages TypeScript's [Mapped Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types) to infer schemas to equivalent static type representations. No additional build process is required. +TypeBox is a type builder library that allows developers to compose complex in-memory JSONSchema objects that can be resolved to static TypeScript types. The schemas produced by TypeBox can be used directly as validation schemas or reflected upon by navigating the standard JSONSchema properties at runtime. -TypeBox can be used as a tool to build and validate complex schemas, or integrated into RPC or REST services to help validate data received over the wire or published directly to consumers to service as developer documentation. +TypeBox can be used as a simple tool to build complex schemas or integrated into RPC or REST services to help validate JSON data received over the wire. -Note that TypeBox does not provide any mechanisms for validating JSONSchema. Please refer to libraries such as [AJV](https://www.npmjs.com/package/ajv) or similar to validate the schemas created with this library. +TypeBox does not provide any mechanism for validating JSONSchema. Please refer to libraries such as [AJV](https://www.npmjs.com/package/ajv) or similar to validate the schemas created with this library. Requires TypeScript 3.8.3 and above. @@ -39,13 +38,14 @@ License MIT - [Overview](#Overview) - [Example](#Example) - [Types](#Types) -- [Other Types](#Intrinsics) +- [More Types](#Intrinsics) - [Functions](#Functions) +- [Generics](#Generics) - [Validation](#Validation) ## Example -The following shows the type alias for `Order` and its TypeBox equivalent. +The following shows the general usage. ```typescript import { Type, Static } from '@sinclair/typebox' @@ -96,12 +96,10 @@ JSON.validate(Order, { // IETF | TC39 ? ## Types -TypeBox functions generate JSONschema objects. The following table outlines the TypeScript and JSONSchema equivalence. +TypeBox provides many functions generate JSONschema data types. The following tables list the functions TypeBox provides and their respective TypeScript and JSONSchema equivalents. ### TypeBox > TypeScript -The following types and modifiers are compatible with JSONschema and have both JSONschema and TypeScript representations. - @@ -206,8 +204,6 @@ The following types and modifiers are compatible with JSONschema and have both J ### TypeBox > JSONSchema -The following shows the TypeBox to JSONSchema mappings. The following schemas are returned from each function. -
@@ -264,7 +260,7 @@ The following shows the TypeBox to JSONSchema mappings. The following schemas ar - + @@ -303,13 +299,13 @@ The following shows the TypeBox to JSONSchema mappings. The following schemas ar -## Other Types +## More Types -TypeBox provides some non-standard JSONSchema functions that TypeBox refers to as Intrinsic types. While these types cannot be used with JSONSchema, they do provide similar reflection and introspection metadata for expressing function signatures with TypeBox. +In addition to the JSONSchema functions, TypeBox also provides some non-standard schemas that provide reflectable metadata for function signatures. These functions allow TypeBox to express `function` and `constructor` signatures where the arguments and return types may be JSONSchema. - See [Functions](#Functions) section for more details. +For more information on their use, see the [Functions](#Functions) and [Generics](#Generics) sections below. -### TypeBox > Intrinsics +### TypeBox > TypeScript
Tupleconst T = Type.Union(Type.Number(), Type.String())const T = Type.Tuple(Type.Number(), Type.String()) { type: "array", items: [{type: 'string'}, {type: 'number'}], additionalItems: false, minItems: 2, maxItems: 2 }
@@ -324,7 +320,12 @@ TypeBox provides some non-standard JSONSchema functions that TypeBox refers to a - + + + + + + @@ -343,7 +344,7 @@ TypeBox provides some non-standard JSONSchema functions that TypeBox refers to a
Function const T = Type.Function([Type.String()], Type.String()) type T = (arg0: string) => string
Constructorconst T = Type.Constructor([Type.String()], Type.String())type T = new (arg0: string) => string
Promise const T = Type.Promise(Type.String())
-### TypeBox > Non Schema +### TypeBox > TypeBox Schema @@ -358,7 +359,12 @@ TypeBox provides some non-standard JSONSchema functions that TypeBox refers to a - + + + + + + @@ -381,13 +387,12 @@ TypeBox provides some non-standard JSONSchema functions that TypeBox refers to a ## Functions -TypeBox provides some capabilities for building typed function signatures. It is important to note however that unlike the other functions available on `Type` the `Type.Function(...)` and other intrinsic types do not produce valid JSONSchema. However, the types returned from `Type.Function(...)` may be comprised of schemas that describe its `arguments` and `return` types. Consider the following TypeScript and TypeBox variants. +The following demonstrates creating function signatures for the following TypeScript types. + +### TypeScript ```typescript - -// TypeScript - -type T0 = (a0: number, a0: number) => number; +type T0 = (a0: number, a1: string) => boolean; type T1 = (a0: string, a1: () => string) => void; @@ -395,28 +400,106 @@ type T2 = (a0: string) => Promise; type T3 = () => () => string; -// Convention +type T4 = new () => string +``` -Type.Function([...Arguments], ReturnType) +### TypeBox -// TypeBox - -const T0 = Type.Function([Type.Number(), Type.Number()], Type.Number()) +```typescript +const T0 = Type.Function([Type.Number(), Type.String()], Type.Boolean()) const T1 = Type.Function([Type.String(), Type.Function([], Type.String())], Type.Void()) const T2 = Type.Function([Type.String()], Type.Promise(Type.Number())) const T3 = Type.Function([], Type.Function([], Type.String())) + +const T4 = Type.Constructor([], Type.String()) ``` + + +## Generics + +Generic function signatures can be composed with TypeScript functions with [Generic Constraints](https://www.typescriptlang.org/docs/handbook/generics.html#generic-constraints). + +### TypeScript +```typescript +type ToString = (t: T) => string +``` +### TypeBox +```typescript +import { Type, Static, TStatic } from '@sinclair/typebox' + +const ToString = (T: G) => Type.Function([T], Type.String()) +``` +However, it's not possible to statically infer what type `ToString` is without first creating some specialized variant of it. The following creates a specialization called `NumberToString`. +```typescript +const NumberToString = ToString(Type.Number()) + +type X = Static + +// X is (arg0: number) => string +``` + To take things a bit further, the following code contains some generic TypeScript REST setup with controllers that take some generic resource of type `T`. Below this we expresses that same setup using TypeBox. The resulting type `IRecordController` contains reflectable metadata about the `RecordController`. +### TypeScript +```typescript +interface IController { + get (): Promise + post (resource: T): Promise + put (resource: T): Promise + delete (resource: T): Promise +} + +interface Record { + key: string + value: string +} + +class StringController implements IController { + async get (): Promise { throw 'not implemented' } + async post (resource: Record): Promise { /* */ } + async put (resource: Record): Promise { /* */ } + async delete(resource: Record): Promise { /* */ } +} +``` + +### TypeBox + +```typescript +import { Type, Static, TStatic } from '@sinclair/typebox' + +const IController = (T: G) => Type.Object({ + get: Type.Function([], Type.Promise(T)), + post: Type.Function([T], Type.Promise(Type.Void())), + put: Type.Function([T], Type.Promise(Type.Void())), + delete: Type.Function([T], Type.Promise(Type.Void())), +}) + +type Record = Static +const Record = Type.Object({ + key: Type.String(), + value: Type.String() +}) + +type IRecordController = Static +const IRecordController = IController(Record) + +class RecordController implements IRecordController { + async get (): Promise { throw 'not implemented' } + async post (resource: Record): Promise { /* */ } + async put (resource: Record): Promise { /* */ } + async delete(resource: Record): Promise { /* */ } +} + +// Reflect !! +console.log(IRecordController) +``` ## Validation -TypeBox does not provide any mechanism for validating JSONSchema out of the box. Users are expected to bring their own JSONSchema validation library. The following demonstrates how you might enable validation with the AJV npm module. - -### General +The following uses the library [Ajv](https://www.npmjs.com/package/ajv) to validate a type. ```typescript import * Ajv from 'ajv' @@ -427,49 +510,3 @@ ajv.validate(Type.String(), 'hello') // true ajv.validate(Type.String(), 123) // false ``` - -### Runtime Type Validation - -The following demonstrates how you might want to approach runtime type validation with TypeBox. The following -code creates a function that takes a signature type `S` which is used to infer function arguments. The body -of the function validates with the signatures `arguments` and `returns` schemas against values passed by the -caller. - -```typescript -import { Type, Static, TFunction } from '@sinclair/typebox' - -// Some validation function. -declare function validate(schema: any, data: any): boolean; - -// A function that returns a closure that validates its -// arguments and return value from the given signature. -function Func(signature: S, func: Static): Static { - const validator = (...params: any[]) => { - params.forEach((param, index) => { - if(!validate(signature.arguments[index], param)) { - console.log('error on argument', index) - } - }) - const result = (func as Function)(...params); - if(!validate(signature.return, result)) { - console.log('error on return') - } - return result - } - return validator as Static -} - -// Create some function. -const Add = Func( - Type.Function([ - Type.Number(), - Type.Number() - ], Type.Number()), - (a, b) => { - return a + b - }) - -// Call it -Add(20, 30) - -``` \ No newline at end of file diff --git a/src/typebox.ts b/src/typebox.ts index 0c4c528..bed4211 100644 --- a/src/typebox.ts +++ b/src/typebox.ts @@ -37,14 +37,14 @@ function reflect(value: any): 'string' | 'number' | 'boolean' | 'unknown' { // #region TIntrinsics -interface TFunction8 { type: 'function', arguments: [T0, T1, T2, T3, T4, T5, T6, T7], return: U } -interface TFunction7 { type: 'function', arguments: [T0, T1, T2, T3, T4, T5, T6], return: U } -interface TFunction6 { type: 'function', arguments: [T0, T1, T2, T3, T4, T5], return: U } -interface TFunction5 { type: 'function', arguments: [T0, T1, T2, T3, T4], return: U } -interface TFunction4 { type: 'function', arguments: [T0, T1, T2, T3], return: U } -interface TFunction3 { type: 'function', arguments: [T0, T1, T2], return: U } -interface TFunction2 { type: 'function', arguments: [T0, T1], return: U } -interface TFunction1 { type: 'function', arguments: [T0], return: U } +interface TFunction8 { type: 'function', arguments: [T0, T1, T2, T3, T4, T5, T6, T7], returns: U } +interface TFunction7 { type: 'function', arguments: [T0, T1, T2, T3, T4, T5, T6], returns: U } +interface TFunction6 { type: 'function', arguments: [T0, T1, T2, T3, T4, T5], returns: U } +interface TFunction5 { type: 'function', arguments: [T0, T1, T2, T3, T4], returns: U } +interface TFunction4 { type: 'function', arguments: [T0, T1, T2, T3], returns: U } +interface TFunction3 { type: 'function', arguments: [T0, T1, T2], returns: U } +interface TFunction2 { type: 'function', arguments: [T0, T1], returns: U } +interface TFunction1 { type: 'function', arguments: [T0], returns: U } interface TFunction0 { type: 'function', arguments: [], returns: U } export type TFunction = TFunction8 | TFunction7 | @@ -56,7 +56,26 @@ export type TFunction = TFunction8 | TFunction0 -export type TIntrinsic = TFunction | TVoid | TUndefined | TPromise +interface TConstructor8 { type: 'constructor', arguments: [T0, T1, T2, T3, T4, T5, T6, T7], returns: U } +interface TConstructor7 { type: 'constructor', arguments: [T0, T1, T2, T3, T4, T5, T6], returns: U } +interface TConstructor6 { type: 'constructor', arguments: [T0, T1, T2, T3, T4, T5], returns: U } +interface TConstructor5 { type: 'constructor', arguments: [T0, T1, T2, T3, T4], returns: U } +interface TConstructor4 { type: 'constructor', arguments: [T0, T1, T2, T3], returns: U } +interface TConstructor3 { type: 'constructor', arguments: [T0, T1, T2], returns: U } +interface TConstructor2 { type: 'constructor', arguments: [T0, T1], returns: U } +interface TConstructor1 { type: 'constructor', arguments: [T0], returns: U } +interface TConstructor0 { type: 'constructor', arguments: [], returns: U } +export type TConstructor = TConstructor8 | + TConstructor7 | + TConstructor6 | + TConstructor5 | + TConstructor4 | + TConstructor3 | + TConstructor2 | + TConstructor1 | + TConstructor0 + +export type TIntrinsic = TFunction | TConstructor | TPromise | TVoid | TUndefined export interface TPromise { type: 'promise', item: T } export interface TVoid { type: 'void' } export interface TUndefined { type: 'undefined' } @@ -123,8 +142,8 @@ export type TComposite = TIntersect | TUnion | TTuple // #region TModifier -export type TOptional = T & { modifier: 'optional' } -export type TReadonly = T & { modifier: 'readonly' } +export type TOptional = T & { modifier: 'optional' } +export type TReadonly = T & { modifier: 'readonly' } export type TModifier = TOptional | TReadonly // #endregion @@ -141,10 +160,10 @@ export type TLiteral = TStringLiteral | TNumberLiteral | TBoolea export interface TStringLiteral { type: 'string', enum: [T] } export interface TNumberLiteral { type: 'number', enum: [T] } export interface TBooleanLiteral { type: 'boolean', enum: [T] } -export interface TProperties { [key: string]: TSchema | TUnion | TIntersect | TTuple | TOptional | TReadonly } +export interface TProperties { [key: string]: TSchema | TComposite | TOptional | TReadonly } export interface TObject { type: 'object', properties: T, required: string[] } -export interface TMap { type: 'object', additionalProperties: T } -export interface TArray { type: 'array', items: T } +export interface TMap { type: 'object', additionalProperties: T } +export interface TArray { type: 'array', items: T } export interface TNumber { type: 'number' } export interface TString { type: 'string' } export interface TBoolean { type: 'boolean' } @@ -172,8 +191,22 @@ type StaticFunction = T extends TFunction0 ? () => Static : never; + +type StaticConstructor = + T extends TConstructor8 ? new (arg0: Static, arg1: Static, arg2: Static, arg3: Static, arg4: Static, arg5: Static, arg6: Static, arg7: Static) => Static : + T extends TConstructor7 ? new (arg0: Static, arg1: Static, arg2: Static, arg3: Static, arg4: Static, arg5: Static, arg6: Static) => Static : + T extends TConstructor6 ? new (arg0: Static, arg1: Static, arg2: Static, arg3: Static, arg4: Static, arg5: Static) => Static : + T extends TConstructor5 ? new (arg0: Static, arg1: Static, arg2: Static, arg3: Static, arg4: Static) => Static : + T extends TConstructor4 ? new (arg0: Static, arg1: Static, arg2: Static, arg3: Static) => Static : + T extends TConstructor3 ? new (arg0: Static, arg1: Static, arg2: Static) => Static : + T extends TConstructor2 ? new (arg0: Static, arg1: Static) => Static : + T extends TConstructor1 ? new (arg0: Static) => Static : + T extends TConstructor0 ? new () => Static : + never; + type StaticInstrinsic = - T extends TFunction ? StaticFunction : + T extends TFunction ? StaticFunction : + T extends TConstructor ? StaticConstructor : T extends TPromise ? Promise> : T extends TVoid ? void : T extends TUndefined ? undefined : @@ -333,25 +366,6 @@ export class Type { return { } } - // #endregion - - // #region PrimitiveExtended - - /** Creates a Promise type. */ - public static Promise(t: T): TPromise { - return { type: 'promise', item: t } - } - - /** Creates a Void type. */ - public static Void(): TVoid { - return { type: 'void' } - } - - /** Creates a Undefined type. */ - public static Undefined(): TUndefined { - return { type: 'undefined' } - } - // #endregion // #region Literal @@ -448,7 +462,8 @@ export class Type { // #endregion - // #region TFunction + // #region TIntrinsic + /** Creates a Function type for the given arguments. */ public static Function(args: [T0, T1, T2, T3, T4, T5, T6, T7], returns: U): TFunction8 /** Creates a Function type for the given arguments. */ @@ -471,7 +486,45 @@ export class Type { public static Function(args: TStatic[], returns: TStatic): TFunction { return { type: 'function', arguments: args, returns: returns } as TFunction } + + /** Creates a Constructor type for the given arguments. */ + public static Constructor(args: [T0, T1, T2, T3, T4, T5, T6, T7], returns: U): TConstructor8 + /** Creates a Constructor type for the given arguments. */ + public static Constructor(args: [T0, T1, T2, T3, T4, T5, T6], returns: U): TConstructor7 + /** Creates a Constructor type for the given arguments. */ + public static Constructor(args: [T0, T1, T2, T3, T4, T5], returns: U): TConstructor6 + /** Creates a Constructor type for the given arguments. */ + public static Constructor(args: [T0, T1, T2, T3, T4], returns: U): TConstructor5 + /** Creates a Constructor type for the given arguments. */ + public static Constructor(args: [T0, T1, T2, T3], returns: U): TConstructor4 + /** Creates a Constructor type for the given arguments. */ + public static Constructor(args: [T0, T1, T2], returns: U): TConstructor3 + /** Creates a Constructor type for the given arguments. */ + public static Constructor(args: [T0, T1], returns: U): TConstructor2 + /** Creates a Constructor type for the given arguments. */ + public static Constructor(args: [T0], returns: U): TConstructor1 + /** Creates a Constructor type for the given arguments. */ + public static Constructor(args: [], returns: U): TConstructor0 + /** Creates a Constructor type for the given arguments. */ + public static Constructor(args: TStatic[], returns: TStatic): TConstructor { + return { type: 'constructor', arguments: args, returns: returns } as TConstructor + } + + /** Creates a Promise type. */ + public static Promise(t: T): TPromise { + return { type: 'promise', item: t } + } + /** Creates a Void type. */ + public static Void(): TVoid { + return { type: 'void' } + } + + /** Creates a Undefined type. */ + public static Undefined(): TUndefined { + return { type: 'undefined' } + } + // #endregion // #region Extended diff --git a/tasks.js b/tasks.js index 55e9b79..ea14daa 100644 --- a/tasks.js +++ b/tasks.js @@ -17,7 +17,7 @@ export async function build() { await shell('tsc -p ./src/tsconfig.json --outDir dist').exec() await folder('dist').add('package.json').exec() await folder('dist').add('readme.md').exec() - await folder('dist').add('license.md').exec() + await folder('dist').add('license').exec() await shell('cd dist && npm pack').exec() // npm publish sinclair-typebox-0.x.x.tgz --access=public
Function const T = Type.Function([Type.String()], Type.Number()) { type: 'function', arguments: [ { type: 'string' } ], returns: { type: 'number' } }
Constructorconst T = Type.Constructor([Type.String()], Type.Number()){ type: 'constructor', arguments: [ { type: 'string' } ], returns: { type: 'number' } }
Promise const T = Type.Promise(Type.String())