@winhillsen/joi-extract-type
Version:
Provides native type extraction from Joi schemas for Typescript
366 lines (324 loc) • 10.2 kB
TypeScript
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>;