joi-extract-type
Version:
Provides native type extraction from Joi schemas for Typescript
581 lines (495 loc) • 20 kB
text/typescript
/** @format */
import * as Joi from '@hapi/joi';
/**
* Helpers
*/
type ArrayType<T> = T extends (infer U)[] ? U : never;
declare module '@hapi/joi' {
/**
* Allow extend() to use Joi types by default
*/
export function extend(
extension: Extension | Extension[],
...extensions: Array<Extension | Extension[]>
): typeof Joi;
/**
* 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;
}
// Operators
type BoxType<B, nT> = B extends Box<infer oT, infer oR> ? Box<nT, oR> : B;
type BoxUnion<B, nT> = B extends Box<infer oT, infer oR> ? Box<oT | nT, oR> : B;
type BoxIntersection<B, nT> = B extends Box<infer oT, infer oR> ? Box<oT & nT, oR> : B;
type BoxReq<B, nR extends boolean> = B extends Box<infer oT, infer oR> ? Box<oT, nR> : B;
type BoxSchema = Box<any, boolean>;
/**
* Every Schema that implements the Box to allow the extraction
*/
type BoxedPrimitive<T extends BoxSchema = any> =
| BoxAnySchema<T>
| BoxStringSchema<T>
| BoxNumberSchema<T>
| BoxBooleanSchema<T>
| BoxDateSchema<T>
| BoxFunctionSchema<T>
| BoxArraySchema<T>
| BoxObjectSchema<T>
| BoxAlternativesSchema<T>;
// Base types
type primitiveType = string | number | boolean | Function | Date | undefined | null | void;
export type mappedSchema = SchemaLike | BoxedPrimitive;
export type mappedSchemaMap = { [K: string]: mappedSchema };
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
): ValidationResult<extendsGuard<T, extractType<S>>>;
export function validate<T, S extends mappedSchemaMap>(
value: T,
schema: S,
options: ValidationOptions
): ValidationResult<extendsGuard<T, extractType<S>>>;
export function validate<T, R, S extends mappedSchemaMap>(
value: T,
schema: S,
options: ValidationOptions,
callback: (err: ValidationError, value: extendsGuard<T, extractType<S>>) => R
): R;
export function validate<T, R, S extends mappedSchemaMap>(
value: T,
schema: S,
callback: (err: ValidationError, value: extendsGuard<T, extractType<S>>) => R
): R;
// TODO: concat
// concat(schema: this): this;
// TODO: when
// when(ref: string, options: WhenOptions): BoxAlternativesSchema;
// when(ref: Reference, options: WhenOptions): BoxAlternativesSchema;
// when(ref: Schema, options: WhenSchemaOptions): BoxAlternativesSchema;
// TODO: see if .default union makes sense;
export interface BoxAnySchema<N extends Box<any, boolean>> extends AnySchema {
__schemaTypeLiteral: 'BoxAnySchema';
default<T>(
value: T,
description?: string
): this extends BoxAnySchema<infer B> ? BoxAnySchema<BoxUnion<B, T>> : never;
default(value: any, description?: string): this;
default(): this;
allow<T>(
...values: T[]
): this extends BoxAnySchema<infer B> ? BoxAnySchema<BoxUnion<B, T>> : never;
allow<T>(
values: T[]
): this extends BoxAnySchema<infer B> ? BoxAnySchema<BoxUnion<B, T>> : never;
allow(...values: any[]): this;
allow(values: any[]): this;
valid<T>(
...values: T[]
): this extends BoxAnySchema<infer B> ? BoxAnySchema<BoxType<B, T>> : never;
valid<T>(values: T[]): this extends BoxAnySchema<infer B> ? BoxAnySchema<BoxType<B, T>> : never;
valid(...values: any[]): this;
valid(values: any[]): this;
required(): this extends BoxAnySchema<infer B> ? BoxAnySchema<BoxReq<B, true>> : never;
required(): this;
exist(): this extends BoxAnySchema<infer B> ? BoxAnySchema<BoxReq<B, true>> : never;
exist(): this;
optional(): this extends BoxAnySchema<infer B> ? BoxAnySchema<BoxReq<B, false>> : never;
optional(): this;
}
/**
* String: extraction decorated schema
*/
export interface BoxStringSchema<N extends BoxSchema> extends StringSchema {
__schemaTypeLiteral: 'BoxStringSchema';
default<T extends string>(
value: T,
description?: string
): this extends BoxStringSchema<infer B> ? BoxStringSchema<BoxUnion<B, T>> : never;
default(value: any, description?: string): this;
default(): this;
allow<T>(
...values: T[]
): this extends BoxStringSchema<infer B> ? BoxStringSchema<BoxUnion<B, T>> : never;
allow<T>(
values: T[]
): this extends BoxStringSchema<infer B> ? BoxStringSchema<BoxUnion<B, T>> : never;
allow(...values: any[]): this;
allow(values: any[]): this;
valid<T extends string>(
...values: T[]
): this extends BoxStringSchema<infer B> ? BoxStringSchema<BoxType<B, T>> : never;
valid<T extends string>(
values: T[]
): this extends BoxStringSchema<infer B> ? BoxStringSchema<BoxType<B, T>> : never;
valid(...values: any[]): this;
valid(values: any[]): this;
required(): this extends BoxStringSchema<infer B> ? BoxStringSchema<BoxReq<B, true>> : never;
required(): this;
exist(): this extends BoxStringSchema<infer B> ? BoxStringSchema<BoxReq<B, true>> : never;
exist(): this;
optional(): this extends BoxStringSchema<infer B> ? BoxStringSchema<BoxReq<B, false>> : never;
optional(): this;
}
/**
* Number: extraction decorated schema
*/
export interface BoxNumberSchema<N extends BoxSchema> extends NumberSchema {
__schemaTypeLiteral: 'BoxNumberSchema';
default<T extends number>(
value: T,
description?: string
): this extends BoxNumberSchema<infer B> ? BoxNumberSchema<BoxUnion<B, T>> : never;
default(value: any, description?: string): this;
default(): this;
allow<T>(
...values: T[]
): this extends BoxNumberSchema<infer B> ? BoxNumberSchema<BoxUnion<B, T>> : never;
allow<T>(
values: T[]
): this extends BoxNumberSchema<infer B> ? BoxNumberSchema<BoxUnion<B, T>> : never;
allow(...values: any[]): this;
allow(values: any[]): this;
valid<T extends string>(
...values: T[]
): this extends BoxNumberSchema<infer B> ? BoxNumberSchema<BoxType<B, T>> : never;
valid<T extends string>(
values: T[]
): this extends BoxNumberSchema<infer B> ? BoxNumberSchema<BoxType<B, T>> : never;
valid(...values: any[]): this;
valid(values: any[]): this;
required(): this extends BoxNumberSchema<infer B> ? BoxNumberSchema<BoxReq<B, true>> : never;
required(): this;
exist(): this extends BoxNumberSchema<infer B> ? BoxNumberSchema<BoxReq<B, true>> : never;
exist(): this;
optional(): this extends BoxNumberSchema<infer B> ? BoxNumberSchema<BoxReq<B, false>> : never;
optional(): this;
}
/**
* Boolean: extraction decorated schema
*/
export interface BoxBooleanSchema<N extends BoxSchema> extends BooleanSchema {
__schemaTypeLiteral: 'BoxBooleanSchema';
default<T extends boolean>(
value: T,
description?: string
): this extends BoxBooleanSchema<infer B> ? BoxBooleanSchema<BoxUnion<B, T>> : never;
default(value: any, description?: string): this;
default(): this;
allow<T>(
...values: T[]
): this extends BoxBooleanSchema<infer B> ? BoxBooleanSchema<BoxUnion<B, T>> : never;
allow<T>(
values: T[]
): this extends BoxBooleanSchema<infer B> ? BoxBooleanSchema<BoxUnion<B, T>> : never;
allow(...values: any[]): this;
allow(values: any[]): this;
valid<T extends string>(
...values: T[]
): this extends BoxBooleanSchema<infer B> ? BoxBooleanSchema<BoxType<B, T>> : never;
valid<T extends string>(
values: T[]
): this extends BoxBooleanSchema<infer B> ? BoxBooleanSchema<BoxType<B, T>> : never;
valid(...values: any[]): this;
valid(values: any[]): this;
required(): this extends BoxBooleanSchema<infer B> ? BoxBooleanSchema<BoxReq<B, true>> : never;
required(): this;
exist(): this extends BoxBooleanSchema<infer B> ? BoxBooleanSchema<BoxReq<B, true>> : never;
exist(): this;
optional(): this extends BoxBooleanSchema<infer B> ? BoxBooleanSchema<BoxReq<B, false>> : never;
optional(): this;
}
/**
* Date: extraction decorated schema
*/
export interface BoxDateSchema<N extends BoxSchema> extends DateSchema {
__schemaTypeLiteral: 'BoxDateSchema';
default<T extends Date>(
value: T,
description?: string
): this extends BoxDateSchema<infer B> ? BoxDateSchema<BoxUnion<B, T>> : never;
default(value: any, description?: string): this;
default(): this;
allow<T>(
...values: T[]
): this extends BoxDateSchema<infer B> ? BoxDateSchema<BoxUnion<B, T>> : never;
allow<T>(
values: T[]
): this extends BoxDateSchema<infer B> ? BoxDateSchema<BoxUnion<B, T>> : never;
allow(...values: any[]): this;
allow(values: any[]): this;
valid<T extends string>(
...values: T[]
): this extends BoxDateSchema<infer B> ? BoxDateSchema<BoxType<B, T>> : never;
valid<T extends string>(
values: T[]
): this extends BoxDateSchema<infer B> ? BoxDateSchema<BoxType<B, T>> : never;
valid(...values: any[]): this;
valid(values: any[]): this;
required(): this extends BoxDateSchema<infer B> ? BoxDateSchema<BoxReq<B, true>> : never;
required(): this;
exist(): this extends BoxDateSchema<infer B> ? BoxDateSchema<BoxReq<B, true>> : never;
exist(): this;
optional(): this extends BoxDateSchema<infer B> ? BoxDateSchema<BoxReq<B, false>> : never;
optional(): this;
}
/**
* Function: extraction decorated schema
*/
export interface BoxFunctionSchema<N extends BoxSchema> extends FunctionSchema {
__schemaTypeLiteral: 'BoxFunctionSchema';
allow<T>(
...values: T[]
): this extends BoxFunctionSchema<infer B> ? BoxFunctionSchema<BoxUnion<B, T>> : never;
allow<T>(
values: T[]
): this extends BoxFunctionSchema<infer B> ? BoxFunctionSchema<BoxUnion<B, T>> : never;
allow(...values: any[]): this;
allow(values: any[]): this;
required(): this extends BoxFunctionSchema<infer B>
? BoxFunctionSchema<BoxReq<B, true>>
: never;
required(): this;
exist(): this extends BoxFunctionSchema<infer B> ? BoxFunctionSchema<BoxReq<B, true>> : never;
exist(): this;
optional(): this extends BoxFunctionSchema<infer B>
? BoxFunctionSchema<BoxReq<B, false>>
: never;
optional(): this;
}
/**
* Array: extraction decorated schema
*/
export interface BoxArraySchema<N extends BoxSchema> extends ArraySchema {
__schemaTypeLiteral: 'BoxArraySchema';
default<T extends any[]>(
value: T,
description?: string
): this extends BoxArraySchema<infer B> ? BoxArraySchema<BoxUnion<B, ArrayType<T>>> : never;
default(value: any, description?: string): this;
default(): this;
allow<T>(
...values: T[]
): this extends BoxArraySchema<infer B> ? BoxArraySchema<BoxUnion<B, T>> : never;
allow<T>(
values: T[]
): this extends BoxArraySchema<infer B> ? BoxArraySchema<BoxUnion<B, T>> : never;
allow(...values: any[]): this;
allow(values: any[]): this;
items<T extends mappedSchema>(
type: T
): this extends BoxArraySchema<infer B> ? BoxArraySchema<BoxUnion<B, extractType<T>>> : never;
items(...types: SchemaLike[]): this;
items(types: SchemaLike[]): this;
required(): this extends BoxArraySchema<infer B> ? BoxArraySchema<BoxReq<B, true>> : never;
required(): this;
exist(): this extends BoxArraySchema<infer B> ? BoxArraySchema<BoxReq<B, true>> : never;
exist(): this;
optional(): this extends BoxArraySchema<infer B> ? BoxArraySchema<BoxReq<B, false>> : never;
optional(): this;
}
/**
* Object: extraction decorated schema
*/
export interface BoxObjectSchema<N extends BoxSchema> extends ObjectSchema {
__schemaTypeLiteral: 'BoxObjectSchema';
default<T extends mappedSchemaMap>(
value: T,
description?: string
): this extends BoxObjectSchema<infer B> ? BoxObjectSchema<BoxUnion<B, extractType<T>>> : never;
default(value: any, description?: string): this;
default(): this;
allow<T>(
...values: T[]
): this extends BoxObjectSchema<infer B> ? BoxObjectSchema<BoxUnion<B, T>> : never;
allow<T>(
values: T[]
): this extends BoxObjectSchema<infer B> ? BoxObjectSchema<BoxUnion<B, T>> : never;
allow(...values: any[]): this;
allow(values: any[]): this;
keys<T extends mappedSchemaMap>(
schema: T
): this extends BoxObjectSchema<infer B>
? BoxObjectSchema<BoxIntersection<B, extractMap<T>>>
: never;
keys(schema?: SchemaMap): this;
append<T extends mappedSchemaMap>(
schema: T
): this extends BoxObjectSchema<infer B>
? BoxObjectSchema<BoxIntersection<B, extractMap<T>>>
: never;
append(schema?: SchemaMap): this;
pattern<S extends BoxStringSchema<any>, T extends mappedSchema>(
pattern: S,
schema: T
): this extends BoxObjectSchema<infer B>
? BoxObjectSchema<BoxIntersection<B, extractMap<{ [key in extractType<S>]: T }>>>
: never;
pattern<T extends mappedSchema>(
pattern: RegExp,
schema: T
): this extends BoxObjectSchema<infer B>
? BoxObjectSchema<BoxIntersection<B, extractMap<{ [key: string]: T }>>>
: never;
pattern(pattern: RegExp | SchemaLike, schema: SchemaLike): this;
required(): this extends BoxObjectSchema<infer B> ? BoxObjectSchema<BoxReq<B, true>> : never;
required(): this;
exist(): this extends BoxObjectSchema<infer B> ? BoxObjectSchema<BoxReq<B, true>> : never;
exist(): this;
optional(): this extends BoxObjectSchema<infer B> ? BoxObjectSchema<BoxReq<B, false>> : never;
optional(): this;
}
/**
* Alternatives: extraction decorated schema
*/
export interface BoxAlternativesSchema<N extends BoxSchema> extends AlternativesSchema {
__schemaTypeLiteral: 'BoxAlternativesSchema';
allow<T>(
...values: T[]
): this extends BoxAlternativesSchema<infer B> ? BoxAlternativesSchema<BoxUnion<B, T>> : never;
allow<T>(
values: T[]
): this extends BoxAlternativesSchema<infer B> ? BoxAlternativesSchema<BoxUnion<B, T>> : never;
allow(...values: any[]): this;
allow(values: any[]): this;
try<T extends mappedSchema[]>(
...values: T
): this extends BoxAlternativesSchema<infer O>
? O extends Box<infer oT, infer oR>
? BoxAlternativesSchema<BoxType<O, oT | extractType<T>>>
: BoxAlternativesSchema<Box<extractType<T>, false>>
: BoxAlternativesSchema<Box<extractType<T>, false>>;
try<T extends mappedSchema[]>(
values: T
): this extends BoxAlternativesSchema<infer O>
? O extends Box<infer oT, infer oR>
? BoxAlternativesSchema<BoxType<O, oT | extractType<T>>>
: BoxAlternativesSchema<Box<extractType<T>, false>>
: BoxAlternativesSchema<Box<extractType<T>, false>>;
try(...types: SchemaLike[]): this;
try(types: SchemaLike[]): this;
required(): this extends BoxAlternativesSchema<infer B>
? BoxAlternativesSchema<BoxReq<B, true>>
: never;
required(): this;
exist(): this extends BoxAlternativesSchema<infer B>
? BoxAlternativesSchema<BoxReq<B, true>>
: never;
exist(): this;
optional(): this extends BoxAlternativesSchema<infer B>
? BoxAlternativesSchema<BoxReq<B, false>>
: never;
optional(): this;
when<
R,
T1 extends mappedSchema,
T2 extends mappedSchema,
T extends { then: T1; otherwise: T2 }
>(
ref: R,
defs: T
): this extends BoxAlternativesSchema<infer O>
? O extends Box<infer oT, infer oR>
? BoxAlternativesSchema<
BoxType<O, oT | extractType<T['then']> | extractType<T['otherwise']>>
>
: BoxAlternativesSchema<Box<extractType<T['then']> | extractType<T['otherwise']>, false>>
: BoxAlternativesSchema<Box<extractType<T['then']> | extractType<T['otherwise']>, false>>;
when(ref: string | Reference, options: WhenOptions): this;
when(ref: Schema, options: WhenSchemaOptions): this;
}
// Factory methods.
export function any<T extends any>(): BoxAnySchema<Box<T, false>>;
export function string<T extends string>(): BoxStringSchema<Box<T, false>>;
export function number<T extends number>(): BoxNumberSchema<Box<T, false>>;
export function boolean<T extends boolean>(): BoxBooleanSchema<Box<T, false>>;
export function date<T extends Date>(): BoxDateSchema<Box<T, false>>;
export function func<T extends Function>(): BoxFunctionSchema<Box<T, false>>;
export function array(): BoxArraySchema<Box<never, false>>;
export function object<T extends mappedSchemaMap>(
schema?: T
): BoxObjectSchema<Box<extractMap<T>, false>>;
export function alternatives<T extends mappedSchema[]>(
...alts: T
): BoxAlternativesSchema<Box<extractType<typeof alts[number]>, false>>;
export function alternatives<T extends mappedSchema[]>(
alts: T
): BoxAlternativesSchema<Box<extractType<typeof alts[number]>, false>>;
export function alt<T extends mappedSchema[]>(
...alts: T
): BoxAlternativesSchema<Box<extractType<typeof alts[number]>, false>>;
export function alt<T extends mappedSchema[]>(
alts: T
): BoxAlternativesSchema<Box<extractType<typeof alts[number]>, false>>;
// Required | Optional properties engine
// prettier-ignore
type Required<T, K = keyof T> = {
[] extends BoxedPrimitive<infer B> ? B['R'] extends true ? K : never : never
: never]: true
};
// prettier-ignore
type Optional<T, K = keyof T> = {
[] extends BoxedPrimitive<infer B> ? B['R'] extends false ? K : never : never
: never]: true
};
// prettier-ignore
type extractMap<T extends mappedSchemaMap> =
{ [K in keyof Optional<T>]?: extractType<T[K]> } &
{ [K in keyof Required<T>]: extractType<T[K]> };
type maybeExtractBox<T> = T extends Box<infer O, infer R> ? O : T;
// prettier-ignore
type extractOne<T extends mappedSchema> =
/** Primitive types */
T extends primitiveType ? T :
/** Holds the extracted type */
T extends BoxAnySchema<infer O> ? maybeExtractBox<O> :
T extends BoxBooleanSchema<infer O> ? maybeExtractBox<O> :
T extends BoxStringSchema<infer O> ? maybeExtractBox<O> :
T extends BoxNumberSchema<infer O> ? maybeExtractBox<O> :
T extends BoxDateSchema<infer O> ? maybeExtractBox<O> :
T extends BoxFunctionSchema<infer O> ? maybeExtractBox<O> :
T extends BoxArraySchema<infer O> ? maybeExtractBox<O>[] :
T extends BoxObjectSchema<infer O> ? maybeExtractBox<O> :
T extends BoxAlternativesSchema<infer O> ? maybeExtractBox<O> :
T extends AnySchema ? any :
any;
// prettier-ignore
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> ? (
O extends SchemaLike ? extractOne<O> :
O extends BoxedPrimitive ? extractOne<O> :
O
) :
/**
* Handle Objects as schemas, without Joi.object at the root.
* It needs to come first than mappedSchema.
* It is difficult to avoid it to be inferred from extends clause.
*/
T extends mappedSchemaMap ? extractMap<T> :
/**
* This is the base case for every schema implemented
*/
T extends SchemaLike ? extractOne<T> :
T extends BoxedPrimitive ? extractOne<T> :
/**
* Default case to handle primitives and schemas
*/
extractOne<T>;
}