UNPKG

@winhillsen/joi-extract-type

Version:

Provides native type extraction from Joi schemas for Typescript

366 lines (324 loc) 10.2 kB
export * from "joi"; import * as joi from "joi"; /** * Helpers */ type Map<T> = { [P in keyof T]: T[P] }; type Diff<T, U> = T extends U ? never : T; /** * Field requirements interface */ interface Box<T, R extends boolean> { /** Type the schema holds */ T: T; /** If this attribute is required when inside an object */ R: R; } type BoxType<B, nT> = B extends Box<infer oT, infer oR> ? Box<nT, oR> : B; type BoxReq<B, nR extends boolean> = B extends Box<infer oT, infer oR> ? Box<oT, nR> : B; /** * Every Schema that implements the Box to allow the extraction */ type BoxedPrimitive<T extends Box<any, any> = any> = | StringSchema<T> | NumberSchema<T> | BooleanSchema<T> | DateSchema<T> | FunctionSchema<T>; // | ArraySchema<T> // Base types type primitiveType = | string | number | boolean | Function | Date | undefined | null | void; type thruthyPrimitiveType = NonNullable<primitiveType>; type schemaMap = { [key: string]: mappedSchema }; type mappedSchema = joi.SchemaLike | BoxedPrimitive | mappedSchemaMap; type mappedSchemaMap<T extends schemaMap = any> = { [K in keyof T]: T[K] }; export type extendsGuard<T, S> = S extends T ? S : T; /** * Validation: extraction decorated methods */ export function validate<T, S extends mappedSchemaMap>( value: T, schema: S ): joi.ValidationResult<extendsGuard<T, extractType<S>>>; export function validate<T, S extends mappedSchemaMap>( value: T, schema: joi.SchemaLike, options: joi.ValidationOptions ): joi.ValidationResult<extendsGuard<T, extractType<S>>>; export function validate<T, R, S extends mappedSchemaMap>( value: T, schema: joi.SchemaLike, options: joi.ValidationOptions, callback: ( err: joi.ValidationError, value: extendsGuard<T, extractType<S>> ) => R ): R; export function validate<T, R, S extends mappedSchemaMap>( value: T, schema: joi.SchemaLike, callback: ( err: joi.ValidationError, value: extendsGuard<T, extractType<S>> ) => R ): R; // TODO: concat // concat(schema: this): this; // TODO: when // when(ref: string, options: WhenOptions): AlternativesSchema; // when(ref: Reference, options: WhenOptions): AlternativesSchema; // when(ref: Schema, options: WhenSchemaOptions): AlternativesSchema; // TODO: see if .default union makes sense; /** * String: extraction decorated schema */ export interface StringSchema<N extends Box<string, boolean> = any> extends joi.StringSchema { default(): this; default(value: any, description?: string): this; default<T extends string>( value: T, description?: string ): StringSchema<BoxType<N, N["T"] | T>>; valid<T extends string>( ...values: T[] ): StringSchema<BoxType<N, typeof values[number]>>; valid<T extends string>( values: T[] ): StringSchema<BoxType<N, typeof values[number]>>; valid(...values: any[]): this; valid(values: any[]): this; required(): StringSchema<BoxReq<N, true>>; required(): this; exist(): StringSchema<BoxReq<N, true>>; exist(): this; optional(): StringSchema<BoxReq<N, false>>; optional(): this; } export function string<T extends string>(): StringSchema<{ T: extractType<T>; R: false; }>; /** * Number: extraction decorated schema */ export interface NumberSchema<N extends Box<number, boolean> = any> extends joi.NumberSchema { default(): this; default(value: any, description?: string): this; default<T extends number>( value: T, description?: string ): NumberSchema<BoxType<N, N["T"] | T>>; valid<T extends number>( ...values: T[] ): NumberSchema<BoxType<N, typeof values[number]>>; valid<T extends number>( values: T[] ): NumberSchema<BoxType<N, typeof values[number]>>; valid(...values: any[]): this; valid(values: any[]): this; required(): NumberSchema<BoxReq<N, true>>; required(): this; exist(): NumberSchema<BoxReq<N, true>>; exist(): this; optional(): NumberSchema<BoxReq<N, false>>; optional(): this; } export function number<T extends number>(): NumberSchema< Box<extractType<T>, false> >; /** * Boolean: extraction decorated schema */ export interface BooleanSchema<N extends Box<boolean, boolean> = any> extends joi.BooleanSchema { default(): this; default(value: any, description?: string): this; default<T extends boolean>( value: T, description?: string ): BooleanSchema<BoxType<N, N["T"] | T>>; valid<T extends boolean>( ...values: T[] ): BooleanSchema<BoxType<N, typeof values[number]>>; valid<T extends boolean>( values: T[] ): BooleanSchema<BoxType<N, typeof values[number]>>; valid(...values: any[]): this; valid(values: any[]): this; required(): BooleanSchema<BoxReq<N, true>>; required(): this; exist(): BooleanSchema<BoxReq<N, true>>; exist(): this; optional(): BooleanSchema<BoxReq<N, false>>; optional(): this; } export function boolean<T extends boolean>(): BooleanSchema<Box<T, false>>; /** * Date: extraction decorated schema */ export interface DateSchema<N extends Box<Date, boolean> = any> extends joi.DateSchema { default(): this; default(value: any, description?: string): this; default<T extends Date>( value: T, description?: string ): DateSchema<BoxType<N, N["T"] | T>>; valid<T extends Date>( ...values: T[] ): DateSchema<BoxType<N, typeof values[number]>>; valid<T extends Date>( values: T[] ): DateSchema<BoxType<N, typeof values[number]>>; valid(...values: any[]): this; valid(values: any[]): this; required(): DateSchema<BoxReq<N, true>>; required(): this; exist(): DateSchema<BoxReq<N, true>>; exist(): this; optional(): DateSchema<BoxReq<N, false>>; optional(): this; } export function date<T extends Date>(): DateSchema<Box<T, false>>; // TOOD: implement DecoratedExtractedValue at: // T extends DateSchema // T extends FunctionSchema /** * Function: extraction decorated schema */ export interface FunctionSchema<N extends Box<Function, boolean> = any> extends joi.FunctionSchema { required(): FunctionSchema<BoxReq<N, true>>; required(): this; exist(): FunctionSchema<BoxReq<N, true>>; exist(): this; optional(): FunctionSchema<BoxReq<N, false>>; optional(): this; } export function func<T extends Function>(): FunctionSchema<Box<T, false>>; /** * Array: extraction decorated schema */ export interface ArraySchema<N = never> extends joi.AnySchema { items<T extends mappedSchema>( type: T ): this extends ArraySchema<infer O> ? ArraySchema<extractType<T> | O> : ArraySchema<extractMap<T>>; // items<T extends mappedSchema>(type: T): ( // this extends ArraySchema<infer O> // ? ArraySchema<{ T: extractType<typeof type> | O['T'], R: O['R'] }> // : ArraySchema<{ T: extractMap<typeof type>, R: false }> // ); } /** * Object: extraction decorated schema */ export interface ObjectSchema<N = null> extends joi.AnySchema { keys<T extends mappedSchemaMap>( schema: T ): this extends ObjectSchema<infer O> ? (O extends null ? ObjectSchema<extractMap<T>> : ObjectSchema<extractMap<T> & O>) : ObjectSchema<extractMap<T>>; } export function object<T extends mappedSchemaMap>( schema: T ): ObjectSchema<extractMap<T>>; /** * Alternatives: extraction decorated schema */ export interface AlternativesSchema<T extends mappedSchema = any> extends joi.AlternativesSchema { try<T extends mappedSchema[]>( ...values: T ): AlternativesSchema<extractType<typeof values[number]>>; try<T extends mappedSchema[]>( values: T ): AlternativesSchema<extractType<typeof values[number]>>; try(...types: joi.SchemaLike[]): this; try(types: joi.SchemaLike[]): this; } export function alternatives<T extends mappedSchema[]>( ...alts: T ): AlternativesSchema<extractType<typeof alts[number]>>; export function alternatives<T extends mappedSchema[]>( alts: T ): AlternativesSchema<extractType<typeof alts[number]>>; export function alt<T extends mappedSchema[]>( ...alts: T ): AlternativesSchema<extractType<typeof alts[number]>>; export function alt<T extends mappedSchema[]>( alts: T ): AlternativesSchema<extractType<typeof alts[number]>>; // Required | Optional properties engine type FilterVoid<T extends string | number | symbol, O extends any> = { [K in T extends (string | number | symbol) ? (O[T] extends (null | undefined | void) ? never : T) : never]: O[K] }; type MarkRequired<T, B> = { [K in keyof T]: T[K] extends BoxedPrimitive<infer D> ? (D["R"] extends B ? T[K] : void) : (B extends false ? T[K] : void) }; type Required<T> = FilterVoid<keyof T, MarkRequired<T, true>>; type Optional<T> = FilterVoid<keyof T, MarkRequired<T, false>>; type extractMap<T> = Map< { [K in keyof Optional<T>]?: extractType<T[K]> } & { [K in keyof Required<T>]: extractType<T[K]> } >; type extractOne<T extends mappedSchema> = /** Primitive types */ T extends primitiveType ? T : T extends BooleanSchema<infer O> ? O["T"] : T extends StringSchema<infer O> ? O["T"] : T extends NumberSchema<infer O> ? O["T"] : T extends DateSchema<infer O> ? O["T"] : T extends FunctionSchema<infer O> ? O["T"] /** Holds the extracted type */ : T extends ArraySchema<infer O> ? O[] : T extends ObjectSchema<infer O> ? O : T extends mappedSchemaMap<infer O> ? O /** Supports Joi.alternatives(Schema1, schema2, ...5) */ : T extends AlternativesSchema<infer O> ? O : any; export type extractType<T extends mappedSchema> = /** * Hack to support [Schema1, Schema2, ...N] alternatives notation * Can't use extractType directly here because of cycles: * ``` * T extends Array<infer O> ? extractType<O> : * ^ cycle * ``` */ T extends Array<infer O> ? extractOne<O> : /** * Handle Objects as schemas, without Joi.object at the root */ T extends { [K: string]: mappedSchema } ? extractMap<T> : /** * Default case to handle primitives and schemas */ extractOne<T>;