Skip to content

Commit

Permalink
new: Add new Infer type.
Browse files Browse the repository at this point in the history
  • Loading branch information
milesj committed Aug 29, 2021
1 parent 186d509 commit b0c42db
Show file tree
Hide file tree
Showing 18 changed files with 133 additions and 29 deletions.
9 changes: 4 additions & 5 deletions optimal/src/schemas/bool.ts
@@ -1,15 +1,14 @@
import { createSchema } from '../createSchema';
import { booleanCriteria, commonCriteria } from '../criteria';
import { invariant } from '../helpers';
import { BooleanCriterias, CommonCriterias, Criteria, DefaultValue, Schema } from '../types';
import { CommonCriterias, Criteria, DefaultValue, Options, Schema } from '../types';

export interface BooleanSchema<T = boolean>
extends Schema<T>,
BooleanCriterias<BooleanSchema<T>>,
CommonCriterias<BooleanSchema<T>> {
export interface BooleanSchema<T = boolean> extends Schema<T>, CommonCriterias<BooleanSchema<T>> {
never: () => BooleanSchema<never>;
notNullable: () => BooleanSchema<NonNullable<T>>;
nullable: () => BooleanSchema<T | null>;
onlyFalse: (options?: Options) => BooleanSchema<false>;
onlyTrue: (options?: Options) => BooleanSchema<true>;
}

function validateType(): Criteria<boolean> | void {
Expand Down
14 changes: 8 additions & 6 deletions optimal/src/schemas/schema.ts
Expand Up @@ -15,15 +15,17 @@ function validateType(): Criteria<unknown> | void {
}

// This is similar to shape, but we want to control the validation
export function schema(): ShapeSchema<AnySchema> {
return createSchema<ShapeSchema<AnySchema>>({
export function schema<T extends AnySchema>(): ShapeSchema<T> {
const shape = createSchema<ShapeSchema<T>>({
cast: createObject,
criteria: { ...commonCriteria, ...shapeCriteria },
type: 'shape',
validateType,
}).of({
schema: func<AnySchema['schema']>().notNullable().required(),
type: func<AnySchema['type']>().notNullable().required(),
validate: func<AnySchema['validate']>().notNullable().required(),
});

return shape.of({
schema: func<T['schema']>().notNullable().required(),
type: func<T['type']>().notNullable().required(),
validate: func<T['validate']>().notNullable().required(),
}) as unknown as ShapeSchema<T>;
}
17 changes: 12 additions & 5 deletions optimal/src/types.ts
Expand Up @@ -73,11 +73,6 @@ export interface ArrayCriterias<S> {
// of: <V>(schema: Schema<V>) => S;
}

export interface BooleanCriterias<S> {
onlyFalse: (options?: Options) => S;
onlyTrue: (options?: Options) => S;
}

export interface DateCriterias<S> {
after: (date: MaybeDate, options?: Options) => S;
before: (date: MaybeDate, options?: Options) => S;
Expand Down Expand Up @@ -163,3 +158,15 @@ export type Predicate<T> = (value: T | null | undefined) => boolean;
// Any is required for generics to be typed correctly for consumers
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnySchema = Schema<any, any>;

// INFERR

export type InferFromObject<T> = { [K in keyof T]: Infer<T[K]> };

export type Infer<T> = T extends Schema<infer U>
? U
: T extends Record<string, AnySchema>
? InferFromObject<T>
: T extends AnySchema[]
? InferFromObject<T>
: never;
8 changes: 8 additions & 0 deletions optimal/tests/optimal.test.ts
Expand Up @@ -2,6 +2,7 @@ import {
array,
bool,
func,
Infer,
instance,
number,
object,
Expand All @@ -22,6 +23,7 @@ describe('Optimal', () => {
// This blueprint is based on Webpack's configuration: https://webpack.js.org/configuration/
// Webpack provides a pretty robust example of how to use this library.
const primitive = union<PrimitiveType>(false).of([string(), number(), bool()]);
type PrimitiveTest = Infer<typeof primitive>;

const condition = union<ConditionType>('').of([
string(),
Expand All @@ -30,6 +32,7 @@ describe('Optimal', () => {
array().of(regex()),
object().of(regex()),
]);
type ConditionTest = Infer<typeof condition>;

const rule = shape({
enforce: string('post').oneOf<'post' | 'pre'>(['pre', 'post']),
Expand All @@ -48,6 +51,7 @@ describe('Optimal', () => {
]),
),
});
type RuleTest = Infer<typeof rule>;

type EntryType = Function | Record<string, string[] | string> | string[] | string;
type CrossOriginType = 'anonymous' | 'use-credentials';
Expand Down Expand Up @@ -115,6 +119,10 @@ describe('Optimal', () => {
]),
),
};
type BlueprintTest = Infer<typeof blueprint>;

const blueprintList = [primitive, condition, rule];
type BlueprintListTest = Infer<typeof blueprintList>;

it('errors if a non-object is passed', () => {
expect(() => {
Expand Down
10 changes: 9 additions & 1 deletion optimal/tests/schemas/array.test.ts
@@ -1,4 +1,4 @@
import { array, ArraySchema, string } from '../../src';
import { array, ArraySchema, bool, Infer, number, object, string } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -9,6 +9,14 @@ describe('array()', () => {
schema = array().of(string());
});

const arrayArray = array().of(array().of(string().nullable()));
const numberArray = array().of(number());
const objectArray = array().of(object().of(array().of(bool())));

type ArrayArray = Infer<typeof arrayArray>;
type NumberArray = Infer<typeof numberArray>;
type ObjectArray = Infer<typeof objectArray>;

runCommonTests((defaultValue) => array<string>(defaultValue), ['a', 'b', 'c'], {
defaultValue: [],
});
Expand Down
10 changes: 9 additions & 1 deletion optimal/tests/schemas/bool.test.ts
@@ -1,4 +1,4 @@
import { bool, BooleanSchema } from '../../src';
import { bool, BooleanSchema, Infer } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -9,6 +9,14 @@ describe('bool()', () => {
schema = bool();
});

const anyBool = bool();
const trueBool = bool().onlyTrue();
const falseBool = bool().onlyFalse();

type AnyBool = Infer<typeof anyBool>;
type TrueBool = Infer<typeof trueBool>;
type FalseBool = Infer<typeof falseBool>;

runCommonTests((defaultValue) => bool(defaultValue), true, {
defaultValue: false,
});
Expand Down
10 changes: 9 additions & 1 deletion optimal/tests/schemas/custom.test.ts
@@ -1,4 +1,4 @@
import { custom, CustomCallback, CustomSchema } from '../../src';
import { custom, CustomCallback, CustomSchema, Infer } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -15,6 +15,14 @@ describe('custom()', () => {
schema = custom<string>(cb, 'xyz');
});

const stringCustom = custom(() => 'abc', '');
const arrayCustom = custom(() => [1, 2, 3], [0]);
const tupleCustom = custom<[string, number]>(() => ['a', 1], ['z', 0]);

type StringCustom = Infer<typeof stringCustom>;
type ArrayCustom = Infer<typeof arrayCustom>;
type TupleCustom = Infer<typeof tupleCustom>;

runCommonTests((defaultValue) => custom<string>(cb, defaultValue), 'abc', {
defaultValue: 'xyz',
});
Expand Down
7 changes: 6 additions & 1 deletion optimal/tests/schemas/date.test.ts
@@ -1,4 +1,4 @@
import { date, DateSchema } from '../../src';
import { date, DateSchema, Infer } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -10,6 +10,11 @@ describe('date()', () => {
schema = date();
});

const nullDate = date().nullable();

type AnyDate = Infer<typeof schema>;
type NullDate = Infer<typeof nullDate>;

runCommonTests((defaultValue) => date(defaultValue), now, {
defaultValue: now,
});
Expand Down
9 changes: 8 additions & 1 deletion optimal/tests/schemas/func.test.ts
@@ -1,4 +1,4 @@
import { func, FunctionSchema, UnknownFunction } from '../../src';
import { func, FunctionSchema, Infer, UnknownFunction } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -10,6 +10,13 @@ describe('func()', () => {
schema = func();
});

const nullFunc = func().nullable();
const typedFunc = func<(a: number) => string>();

type AnyFunc = Infer<typeof schema>;
type NullFunc = Infer<typeof nullFunc>;
type TypedFunc = Infer<typeof typedFunc>;

runCommonTests(() => func(), noop, { defaultValue: undefined });

describe('type()', () => {
Expand Down
9 changes: 8 additions & 1 deletion optimal/tests/schemas/instance.test.ts
@@ -1,4 +1,4 @@
import { instance, InstanceSchema } from '../../src';
import { Infer, instance, InstanceSchema } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -13,6 +13,13 @@ describe('instance()', () => {
schema = instance();
});

const nullClass = instance().nullable();
const typedClass = instance().of(Bar).notNullable();

type AnyClass = Infer<typeof schema>;
type NullClass = Infer<typeof nullClass>;
type TypedClass = Infer<typeof typedClass>;

runCommonTests(() => instance().of(Foo), new Foo(), {
defaultValue: null,
});
Expand Down
7 changes: 6 additions & 1 deletion optimal/tests/schemas/number.test.ts
@@ -1,4 +1,4 @@
import { number, NumberSchema } from '../../src';
import { Infer, number, NumberSchema } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -9,6 +9,11 @@ describe('number()', () => {
schema = number();
});

const litNumber = number().oneOf([1, 2, 3]);

type AnyNumber = Infer<typeof schema>;
type LiteralNumber = Infer<typeof litNumber>;

runCommonTests((defaultValue) => number(defaultValue), 123, { defaultValue: 0 });

describe('oneOf()', () => {
Expand Down
9 changes: 8 additions & 1 deletion optimal/tests/schemas/object.test.ts
@@ -1,4 +1,4 @@
import { number, object, ObjectSchema, string } from '../../src';
import { array, Infer, number, object, ObjectSchema, string, tuple } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -9,6 +9,13 @@ describe('object()', () => {
schema = object().of(string());
});

const arrayObject = object().of(array().of(string()));
const objectTupleObject = object().of(object().of(tuple<[string, number]>([string(), number()])));

type StringObject = Infer<typeof schema>;
type ArrayObject = Infer<typeof arrayObject>;
type ObjectTupleObject = Infer<typeof objectTupleObject>;

runCommonTests<Record<string, string>>(
(defaultValue) => object(defaultValue),
{ a: 'b' },
Expand Down
7 changes: 6 additions & 1 deletion optimal/tests/schemas/regex.test.ts
@@ -1,4 +1,4 @@
import { InstanceSchema, regex } from '../../src';
import { Infer, InstanceSchema, regex } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -9,6 +9,11 @@ describe('regex()', () => {
schema = regex();
});

const notNullRegex = regex().notNullable();

type AnyRegex = Infer<typeof schema>;
type NotNullRegex = Infer<typeof notNullRegex>;

runCommonTests(() => regex(), /abc/u, { defaultValue: null });

describe('type()', () => {
Expand Down
7 changes: 6 additions & 1 deletion optimal/tests/schemas/schema.test.ts
@@ -1,4 +1,4 @@
import { AnySchema, schema as schemaFunc, ShapeSchema } from '../../src';
import { AnySchema, Infer, schema as schemaFunc, ShapeSchema, StringSchema } from '../../src';
import { runInProd } from '../helpers';

describe('schema()', () => {
Expand All @@ -8,6 +8,11 @@ describe('schema()', () => {
schema = schemaFunc();
});

const stringSchema = schemaFunc<StringSchema>();

type AllSchema = Infer<typeof schema>;
type StringsSchema = Infer<typeof stringSchema>;

describe('type()', () => {
it('returns shape type', () => {
expect(schemaFunc().type()).toBe(
Expand Down
15 changes: 14 additions & 1 deletion optimal/tests/schemas/shape.test.ts
@@ -1,4 +1,4 @@
import { bool, number, shape, ShapeSchema, string } from '../../src';
import { bool, Infer, number, shape, ShapeSchema, string } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -17,6 +17,19 @@ describe('shape()', () => {
});
});

const shapeShape = shape({
foo: string(),
bar: shape({
baz: number(),
qux: shape({
wow: bool(),
}),
}),
});

type BaseShape = Infer<typeof schema>;
type ShapeShape = Infer<typeof shapeShape>;

runCommonTests(
() =>
shape({
Expand Down
7 changes: 6 additions & 1 deletion optimal/tests/schemas/string.test.ts
@@ -1,4 +1,4 @@
import { string, StringSchema } from '../../src';
import { Infer, string, StringSchema } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -14,6 +14,11 @@ describe('string()', () => {
schema = string();
});

const litString = string().oneOf(['a', 'b', 'c']);

type AnyString = Infer<typeof schema>;
type LiteralString = Infer<typeof litString>;

runCommonTests((defaultValue) => string(defaultValue), 'abc', {
defaultValue: '',
});
Expand Down
4 changes: 3 additions & 1 deletion optimal/tests/schemas/tuple.test.ts
@@ -1,4 +1,4 @@
import { array, bool, number, object, string, tuple, TupleSchema } from '../../src';
import { array, bool, Infer, number, object, string, tuple, TupleSchema } from '../../src';
import { runInProd } from '../helpers';
import { runCommonTests } from './runCommonTests';

Expand All @@ -17,6 +17,8 @@ describe('tuple()', () => {
]);
});

type AllTuple = Infer<typeof schema>;

runCommonTests<Tuple>(
() =>
tuple([
Expand Down
3 changes: 3 additions & 0 deletions optimal/tests/schemas/union.test.ts
Expand Up @@ -2,6 +2,7 @@ import {
array,
bool,
custom,
Infer,
instance,
number,
object,
Expand Down Expand Up @@ -39,6 +40,8 @@ describe('union()', () => {
]);
});

type AllUnion = Infer<typeof schema>;

runCommonTests<Union>(
(defaultValue) =>
union<Union>(defaultValue!).of([
Expand Down

0 comments on commit b0c42db

Please sign in to comment.