Repository: jquense/yup
Pull request #1540 (2021-12-28 16:35:57)

feat: More intuitive Object generics, faster types
Changes the object generics to store the derived type instead of the object shape. This imposes a few limitations on type accuracy at the edges, but dramatically speeds up the type processing by `tsc` on the command line and in editor. It also removes the need for the `SchemaOf` helper which worked...poorly. 

Instead the `ObjectSchema` class accepts a plain type as its first generic:

```diff
interface Folder {
  id: ObjectId,
  label: string,
  files?: File[]
}

- const folder: SchemaOf<Folder, ObjectId | File> = object({
-   id: mixed<ObjectId>().defined(),
-   label: string().defined(), 
-   files: array(mixed<File>().defined()) 
- })
+ const folder: ObjectSchema<Folder> = object({ 
+  id: mixed<ObjectId>().defined(),
+  label: string().defined(), 
+  files: array(mixed<File>().defined())
+ })
```

It's a small diff, but big improvement in type accuracy and usability, especially with custom schema for class instances. 
> Note that the generics on the `object()` factory method are still the "object shape, meaning `object<Folder>()` won't work as expected. This is a compromise between the two strategies for handling generics and allows for an accurate type on `object().getDefault()`

A number of the improvements here are made possible by simplifications to yup's API and logic, this introduces a few breaking changes though most are small and easily migrated from.

## Nullability and presence

This is the largest, and likely most disruptive change. Prior yup allowed for patterns like:

```ts
const nullableRequiredString = string().nullable().required()

nullableRequiredString.cast(null) // -> null

nullableRequiredString.validate(null) // ValidationError("this is required and cannot be null")
```

This may seem unintuitive behavior (and it is) but allowed for a common client side validation case, where we want to use a single schema to parse server data, as well as validate user input. In other words, a server might return invalid "default" values that should still fail when trying to submit. 

Now, `nullable()`, `defined` and `required` are all mutually dependent methods. Meaning `string().nullable().defined().required()` produces a schema where the value must be a string, and not `null` or `undefined`. The effect of this is that the type of a `cast()` is now accurate and the same as the type returned from `validate`. 

### When signature

The following API has been simplified to be clearer, and easier to type 

```diff
string().when('foo', {
  is: 'bar',
-  then: string().required()
+ then: schema => schema.required()
})
```

# Concat behavior

Current concat doesn't work super well and will likely be removed as it's not very useful. It's primary use was supporting the now-defunct `when()` api above. More on this coming later





Patch:
diff --git a/.eslintrc b/.eslintrc
index d531a81..162b657 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -25,7 +25,8 @@
         "jest/no-focused-tests": "error",
         "jest/no-identical-title": "error",
         "jest/prefer-to-have-length": "warn",
-        "jest/valid-expect": "off"
+        "jest/valid-expect": "off",
+        "@typescript-eslint/no-empty-function": "off"
       }
     }
   ]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..fecadfa
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,18 @@
+name: Test
+on:
+  push:
+    branches: [master, next]
+  pull_request:
+    branches: [master, next]
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Setup Node
+        uses: actions/setup-node@v2
+        with:
+          node-version: 'lts/*'
+          cache: 'yarn'
+      - run: yarn install --frozen-lockfile
+      - run: yarn test
diff --git a/jest-sync.config.json b/jest-sync.config.json
index aadc50d..3a95dd6 100644
--- a/jest-sync.config.json
+++ b/jest-sync.config.json
@@ -7,5 +7,5 @@
   "setupFilesAfterEnv": ["./test-setup.js"],
   "roots": ["test"],
   "testRegex": "\\.(t|j)s$",
-  "testPathIgnorePatterns": ["helpers\\.js", "\\.eslintrc\\.js", "types\\.ts"]
+  "testPathIgnorePatterns": ["helpers\\.ts", "\\.eslintrc\\.js", "types\\.ts"]
 }
diff --git a/package.json b/package.json
index c88aa36..57b9f5d 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
     ],
     "testRegex": "\\.(j|t)s$",
     "testPathIgnorePatterns": [
-      "helpers\\.js",
+      "helpers\\.ts",
       "\\.eslintrc\\.js",
       "types\\.ts"
     ]
@@ -73,6 +73,7 @@
     "@babel/core": "^7.15.8",
     "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5",
     "@babel/preset-typescript": "^7.15.0",
+    "@types/jest": "^27.0.3",
     "@typescript-eslint/eslint-plugin": "^4.33.0",
     "@typescript-eslint/parser": "^4.33.0",
     "babel-eslint": "^10.1.0",
diff --git a/src/Condition.ts b/src/Condition.ts
index 2130803..3feb5af 100644
--- a/src/Condition.ts
+++ b/src/Condition.ts
@@ -1,21 +1,21 @@
 import isSchema from './util/isSchema';
 import Reference from './Reference';
-import { SchemaLike } from './types';
+import type { ISchema } from './util/types';
 
-export interface ConditionBuilder<T extends SchemaLike> {
-  (this: T, value: any, schema: T): SchemaLike;
-  (v1: any, v2: any, schema: T): SchemaLike;
-  (v1: any, v2: any, v3: any, schema: T): SchemaLike;
-  (v1: any, v2: any, v3: any, v4: any, schema: T): SchemaLike;
+export interface ConditionBuilder<T extends ISchema<any, any>> {
+  (this: T, value: any, schema: T): ISchema<any, any> | void;
+  (v1: any, v2: any, schema: T): ISchema<any, any> | void;
+  (v1: any, v2: any, v3: any, schema: T): ISchema<any, any> | void;
+  (v1: any, v2: any, v3: any, v4: any, schema: T): ISchema<any, any> | void;
 }
 
-export type ConditionConfig<T extends SchemaLike> = {
+export type ConditionConfig<T extends ISchema<any>> = {
   is: any | ((...values: any[]) => boolean);
-  then?: SchemaLike | ((schema: T) => SchemaLike);
-  otherwise?: SchemaLike | ((schema: T) => SchemaLike);
+  then?: (schema: T) => ISchema<any>;
+  otherwise?: (schema: T) => ISchema<any>;
 };
 
-export type ConditionOptions<T extends SchemaLike> =
+export type ConditionOptions<T extends ISchema<any, any>> =
   | ConditionBuilder<T>
   | ConditionConfig<T>;
 
@@ -25,7 +25,7 @@ export type ResolveOptions<TContext = any> = {
   context?: TContext;
 };
 
-class Condition<T extends SchemaLike = SchemaLike> {
+class Condition<T extends ISchema<any, any> = ISchema<any, any>> {
   fn: ConditionBuilder<T>;
 
   constructor(public refs: Reference[], options: ConditionOptions<T>) {
@@ -52,18 +52,17 @@ class Condition<T extends SchemaLike = SchemaLike> {
         : (...values: any[]) => values.every((value) => value === is);
 
     this.fn = function (...args: any[]) {
-      let options = args.pop();
+      let _opts = args.pop();
       let schema = args.pop();
       let branch = check(...args) ? then : otherwise;
 
-      if (!branch) return undefined;
-      if (typeof branch === 'function') return branch(schema);
-      return schema.concat(branch.resolve(options));
+      return branch?.(schema) ?? schema;
     };
   }
 
   resolve(base: T, options: ResolveOptions) {
     let values = this.refs.map((ref) =>
+      // TODO: ? operator here?
       ref.getValue(options?.value, options?.parent, options?.context),
     );
 
diff --git a/src/Lazy.ts b/src/Lazy.ts
index 953aa4b..6472f0b 100644
--- a/src/Lazy.ts
+++ b/src/Lazy.ts
@@ -1,63 +1,72 @@
 import isSchema from './util/isSchema';
-import type { Callback, ValidateOptions } from './types';
+import type { AnyObject, Callback, ValidateOptions } from './types';
 import type { ResolveOptions } from './Condition';
 
 import type {
-  AnySchema,
   CastOptions,
-  ConfigOf,
   SchemaFieldDescription,
   SchemaLazyDescription,
 } from './schema';
-import { Config, TypedSchema, TypeOf } from './util/types';
-
-export type LazyBuilder<T extends AnySchema = any> = (
+import { Flags, ISchema } from './util/types';
+import { BaseSchema } from '.';
+
+export type LazyBuilder<
+  T,
+  TContext = AnyObject,
+  TDefault = any,
+  TFlags extends Flags = any,
+> = (
   value: any,
   options: ResolveOptions,
-) => T;
-
-export function create<T extends AnySchema>(builder: LazyBuilder<T>) {
-  return new Lazy(builder);
+) => ISchema<T, TContext, TFlags, TDefault>;
+
+export function create<
+  T,
+  TContext = AnyObject,
+  TFlags extends Flags = any,
+  TDefault = any,
+>(builder: LazyBuilder<T, TContext, TDefault, TFlags>) {
+  return new Lazy<T, TContext, TDefault, TFlags>(builder);
 }
 
-export type LazyReturnValue<T> = T extends Lazy<infer TSchema>
-  ? TSchema
-  : never;
-
-export type LazyType<T> = LazyReturnValue<T> extends TypedSchema
-  ? TypeOf<LazyReturnValue<T>>
-  : never;
-
 export interface LazySpec {
   meta: Record<string, unknown> | undefined;
 }
 
-class Lazy<T extends AnySchema, TConfig extends Config = ConfigOf<T>>
-  implements TypedSchema {
+class Lazy<T, TContext = AnyObject, TDefault = any, TFlags extends Flags = any>
+  implements ISchema<T, TContext, TFlags, TDefault>
+{
   type = 'lazy' as const;
 
   __isYupSchema__ = true;
 
-  readonly __type!: T['__type'];
-  readonly __outputType!: T['__outputType'];
+  declare readonly __outputType: T;
+  declare readonly __context: TContext;
+  declare readonly __flags: TFlags;
+  declare readonly __default: TDefault;
 
   spec: LazySpec;
 
-  constructor(private builder: LazyBuilder<T>) {
+  constructor(private builder: LazyBuilder<T, TContext, TDefault, TFlags>) {
     this.spec = { meta: undefined };
   }
 
-  clone(): Lazy<T, TConfig> {
-    const next = new Lazy(this.builder);
+  clone(): Lazy<T, TContext, TDefault, TFlags> {
+    const next = create(this.builder);
     next.spec = { ...this.spec };
     return next;
   }
 
   private _resolve = (
     value: any,
-    options: ResolveOptions<TConfig['context']> = {},
-  ): T => {
-    let schema = this.builder(value, options);
+    options: ResolveOptions<TContext> = {},
+  ): BaseSchema<T, TContext, TDefault, TFlags> => {
+    let schema = this.builder(value, options) as BaseSchema<
+      T,
+      TContext,
+      TDefault,
+      TFlags
+    >;
 
     if (!isSchema(schema))
       throw new TypeError('lazy() functions must return a valid schema');
@@ -65,11 +74,11 @@ class Lazy<T extends AnySchema, TConfig extends Config = ConfigOf<T>>
     return schema.resolve(options);
   };
 
-  resolve(options: ResolveOptions<TConfig['context']>) {
+  resolve(options: ResolveOptions<TContext>) {
     return this._resolve(options.value, options);
   }
 
-  cast(value: any, options?: CastOptions<TConfig['context']>): T['__type'] {
+  cast(value: any, options?: CastOptions<TContext>): T {
     return this._resolve(value, options).cast(value, options);
   }
 
@@ -77,44 +86,37 @@ class Lazy<T extends AnySchema, TConfig extends Config = ConfigOf<T>>
     value: any,
     options?: ValidateOptions,
     maybeCb?: Callback,
-  ): T['__outputType'] {
+  ): Promise<T> {
     // @ts-expect-error missing public callback on type
     return this._resolve(value, options).validate(value, options, maybeCb);
   }
 
-  validateSync(
-    value: any,
-    options?: ValidateOptions<TConfig['context']>,
-  ): T['__outputType'] {
+  validateSync(value: any, options?: ValidateOptions<TContext>): T {
     return this._resolve(value, options).validateSync(value, options);
   }
 
-  validateAt(
-    path: string,
-    value: any,
-    options?: ValidateOptions<TConfig['context']>,
-  ) {
+  validateAt(path: string, value: any, options?: ValidateOptions<TContext>) {
     return this._resolve(value, options).validateAt(path, value, options);
   }
 
   validateSyncAt(
     path: string,
     value: any,
-    options?: ValidateOptions<TConfig['context']>,
+    options?: ValidateOptions<TContext>,
   ) {
     return this._resolve(value, options).validateSyncAt(path, value, options);
   }
 
-  isValid(value: any, options?: ValidateOptions<TConfig['context']>) {
+  isValid(value: any, options?: ValidateOptions<TContext>) {
     return this._resolve(value, options).isValid(value, options);
   }
 
-  isValidSync(value: any, options?: ValidateOptions<TConfig['context']>) {
+  isValidSync(value: any, options?: ValidateOptions<TContext>) {
     return this._resolve(value, options).isValidSync(value, options);
   }
 
   describe(
-    options?: ResolveOptions<TConfig['context']>,
+    options?: ResolveOptions<TContext>,
   ): SchemaLazyDescription | SchemaFieldDescription {
     return options
       ? this.resolve(options).describe(options)
@@ -122,7 +124,7 @@ class Lazy<T extends AnySchema, TConfig extends Config = ConfigOf<T>>
   }
 
   meta(): Record<string, unknown> | undefined;
-  meta(obj: Record<string, unknown>): Lazy<T, TConfig>;
+  meta(obj: Record<string, unknown>): Lazy<T, TContext, TDefault, TFlags>;
   meta(...args: [Record<string, unknown>?]) {
     if (args.length === 0) return this.spec.meta;
 
diff --git a/src/array.ts b/src/array.ts
index 8936ad8..7e01b8c 100644
--- a/src/array.ts
+++ b/src/array.ts
@@ -13,39 +13,36 @@ import type {
 import ValidationError from './ValidationError';
 import type Reference from './Reference';
 import {
-  Asserts,
-  Config,
   Defined,
+  Flags,
   NotNull,
   SetFlag,
   Thunk,
   ToggleDefault,
+  ISchema,
+  UnsetFlag,
 } from './util/types';
-import BaseSchema, {
-  AnySchema,
-  SchemaInnerTypeDescription,
-  SchemaSpec,
-} from './schema';
-import Lazy from './Lazy';
+import BaseSchema, { SchemaInnerTypeDescription, SchemaSpec } from './schema';
 import { ResolveOptions } from './Condition';
 
 export type RejectorFn = (value: any, index: number, array: any[]) => boolean;
 
-export function create<
-  C extends AnyObject = AnyObject,
-  T extends AnySchema | Lazy<any, any> = AnySchema
->(type?: T) {
-  return new ArraySchema<T, Config<C>>(type);
+export function create<C extends AnyObject = AnyObject, T = any>(
+  type?: ISchema<T, C>,
+) {
+  return new ArraySchema<T, C>(type as any);
 }
 
 export default class ArraySchema<
-  T extends AnySchema | Lazy<any, any>,
-  C extends Config<any, any> = Config,
-  TIn extends Maybe<Asserts<T>[]> = Asserts<T>[] | undefined
-> extends BaseSchema<TIn, C> {
-  innerType?: T;
-
-  constructor(type?: T) {
+  T,
+  TContext,
+  TDefault = undefined,
+  TFlags extends Flags = '',
+  TIn extends any[] | null | undefined = T[] | undefined,
+> extends BaseSchema<TIn, TContext, TDefault, TFlags> {
+  innerType?: ISchema<T, TContext>;
+
+  constructor(type?: ISchema<T, TContext>) {
     super({ type: 'array' });
 
     // `undefined` specifically means uninitialized, as opposed to
@@ -74,7 +71,7 @@ export default class ArraySchema<
     return this.innerType;
   }
 
-  protected _cast(_value: any, _opts: InternalOptions<C>) {
+  protected _cast(_value: any, _opts: InternalOptions<TContext>) {
     const value = super._cast(_value, _opts);
 
     //should ignore nulls here
@@ -98,7 +95,7 @@ export default class ArraySchema<
 
   protected _validate(
     _value: any,
-    options: InternalOptions<C> = {},
+    options: InternalOptions<TContext> = {},
     callback: Callback,
   ) {
     let errors = [] as ValidationError[];
@@ -181,7 +178,7 @@ export default class ArraySchema<
     return next;
   }
 
-  of<TInner extends AnySchema>(schema: TInner): ArraySchema<TInner> {
+  of<U>(schema: ISchema<U, TContext>): ArraySchema<U, TContext, TFlags> {
     // FIXME: this should return a new instance of array without the default to be
     let next = this.clone();
 
@@ -240,14 +237,16 @@ export default class ArraySchema<
     });
   }
 
-  ensure(): ArraySchema<T, SetFlag<C, 'd'>, NonNullable<TIn>> {
-    return this.default(() => ([] as any) as TIn).transform(
+  //  ArraySchema<T, TContext, T[], SetFlag<TFlags, 'd'>, NonNullable<TIn>>
+
+  ensure() {
+    return this.default<TIn>(() => [] as any).transform(
       (val: TIn, original: any) => {
         // We don't want to return `null` for nullable schema
         if (this._typeCheck(val)) return val;
         return original == null ? [] : [].concat(original);
       },
-    ) as any;
+    );
   }
 
   compact(rejector?: RejectorFn) {
@@ -260,7 +259,7 @@ export default class ArraySchema<
     );
   }
 
-  describe(options?: ResolveOptions<C['context']>) {
+  describe(options?: ResolveOptions<TContext>) {
     let base = super.describe() as SchemaInnerTypeDescription;
     if (this.innerType) {
       let innerOptions = options;
@@ -275,43 +274,51 @@ export default class ArraySchema<
     }
     return base;
   }
-
-  nullable(isNullable?: true): ArraySchema<T, C, TIn | null>;
-  nullable(isNullable: false): ArraySchema<T, C, Exclude<TIn, null>>;
-  nullable(isNullable = true): ArraySchema<T, C, TIn | null> {
-    return super.nullable(isNullable as any);
-  }
-
-  defined(): DefinedArraySchema<T, C, TIn> {
-    return super.defined();
-  }
-
-  required(msg?: MixedLocale['required']): RequiredArraySchema<T, C, TIn> {
-    return super.required(msg);
-  }
 }
 
 create.prototype = ArraySchema.prototype;
 
-//
-// Interfaces
-//
-
 export default interface ArraySchema<
-  T extends AnySchema | Lazy<any, any>,
-  C extends Config<any, any> = Config,
-  TIn extends Maybe<Asserts<T>[]> = Asserts<T>[] | undefined
-> extends BaseSchema<TIn, C> {
+  T,
+  TContext,
+  TDefault = undefined,
+  TFlags extends Flags = '',
+  TIn extends any[] | null | undefined = T[] | undefined,
+> extends BaseSchema<TIn, TContext, TDefault, TFlags> {
   default<D extends Maybe<TIn>>(
     def: Thunk<D>,
-  ): ArraySchema<T, ToggleDefault<C, D>, TIn>;
-
-  defined(msg?: Message): ArraySchema<T, C, Defined<TIn>>;
-  optional(): ArraySchema<T, C, TIn | undefined>;
-
-  required(msg?: Message): ArraySchema<T, C, NonNullable<TIn>>;
-  notRequired(): ArraySchema<T, C, Maybe<TIn>>;
-
-  nullable(isNullable?: true): ArraySchema<T, C, TIn | null>;
-  nullable(isNullable: false): ArraySchema<T, C, NotNull<TIn>>;
+  ): ArraySchema<T, TContext, D, ToggleDefault<TFlags, D>, TIn>;
+
+  concat<IT, IC, ID, IF extends Flags, IIn extends Maybe<IT[]>>(
+    schema: ArraySchema<IT, IC, ID, IF, IIn>,
+  ): ArraySchema<
+    NonNullable<T> | IT,
+    TContext & IC,
+    Extract<IF, 'd'> extends never ? TDefault : ID,
+    TFlags | IF,
+    IIn
+  >;
+  concat(schema: this): this;
+
+  defined(
+    msg?: Message,
+  ): ArraySchema<T, TContext, TDefault, TFlags, Defined<TIn>>;
+  optional(): ArraySchema<T, TContext, TDefault, TFlags, TIn | undefined>;
+
+  required(
+    msg?: Message,
+  ): ArraySchema<T, TContext, TDefault, TFlags, NonNullable<TIn>>;
+  notRequired(): ArraySchema<T, TContext, TDefault, TFlags, Maybe<TIn>>;
+
+  nullable(
+    msg?: Message,
+  ): ArraySchema<T, TContext, TDefault, TFlags, TIn | null>;
+  nonNullable(): ArraySchema<T, TContext, TDefault, TFlags, NotNull<TIn>>;
+
+  strip(
+    enabled: false,
+  ): ArraySchema<T, TContext, TDefault, UnsetFlag<TFlags, 's'>>;
+  strip(
+    enabled?: true,
+  ): ArraySchema<T, TContext, TDefault, SetFlag<TFlags, 's'>>;
 }
diff --git a/src/boolean.ts b/src/boolean.ts
index 6ea0036..c2d50d5 100644
--- a/src/boolean.ts
+++ b/src/boolean.ts
@@ -1,9 +1,10 @@
 import BaseSchema from './schema';
 import type { AnyObject, Maybe, Message } from './types';
 import type {
-  Config,
   Defined,
+  Flags,
   NotNull,
+  SetFlag,
   Thunk,
   ToggleDefault,
 } from './util/types';
@@ -13,16 +14,18 @@ import isAbsent from './util/isAbsent';
 export function create(): BooleanSchema;
 export function create<
   T extends boolean,
-  TContext = AnyObject
->(): BooleanSchema<T | undefined, Config<TContext>>;
+  TContext = AnyObject,
+>(): BooleanSchema<T | undefined, TContext>;
 export function create() {
   return new BooleanSchema();
 }
 
 export default class BooleanSchema<
   TType extends Maybe<boolean> = boolean | undefined,
-  TConfig extends Config<any, any> = Config
-> extends BaseSchema<TType, TConfig> {
+  TContext = AnyObject,
+  TDefault = undefined,
+  TFlags extends Flags = '',
+> extends BaseSchema<TType, TContext, TDefault, TFlags> {
   constructor() {
     super({ type: 'boolean' });
 
@@ -43,7 +46,9 @@ export default class BooleanSchema<
     return typeof v === 'boolean';
   }
 
-  isTrue(message = locale.isValue): BooleanSchema<TType | true, TConfig> {
+  isTrue(
+    message = locale.isValue,
+  ): BooleanSchema<TType | true, TContext, TFlags> {
     return this.test({
       message,
       name: 'is-value',
@@ -55,7 +60,9 @@ export default class BooleanSchema<
     }) as any;
   }
 
-  isFalse(message = locale.isValue): BooleanSchema<TType | false, TConfig> {
+  isFalse(
+    message = locale.isValue,
+  ): BooleanSchema<TType | false, TContext, TFlags> {
     return this.test({
       message,
       name: 'is-value',
@@ -66,26 +73,42 @@ export default class BooleanSchema<
       },
     }) as any;
   }
-}
-
-create.prototype = BooleanSchema.prototype;
 
-export default interface BooleanSchema<
-  TType extends Maybe<boolean>,
-  TConfig extends Config<any, any> = Config
-> extends BaseSchema<TType, TConfig> {
-  default<D extends Maybe<TType>>(
+  override default<D extends Maybe<TType>>(
     def: Thunk<D>,
-  ): BooleanSchema<TType, ToggleDefault<TConfig, D>>;
-
-  concat<TOther extends BooleanSchema<any, any>>(schema: TOther): TOther;
-
-  defined(msg?: Message): BooleanSchema<Defined<TType>, TConfig>;
-  optional(): BooleanSchema<TType | undefined, TConfig>;
+  ): BooleanSchema<TType, TContext, D, ToggleDefault<TFlags, D>> {
+    return super.default(def);
+  }
 
-  required(msg?: Message): BooleanSchema<NonNullable<TType>, TConfig>;
-  notRequired(): BooleanSchema<Maybe<TType>, TConfig>;
+  // concat<TOther extends BooleanSchema<any, any>>(schema: TOther): TOther;
+  defined(
+    msg?: Message,
+  ): BooleanSchema<Defined<TType>, TContext, TDefault, TFlags> {
+    return super.defined(msg);
+  }
+  optional(): BooleanSchema<TType | undefined, TContext, TDefault, TFlags> {
+    return super.defined();
+  }
+  required(
+    msg?: Message,
+  ): BooleanSchema<NonNullable<TType>, TContext, TDefault, TFlags> {
+    return super.required(msg);
+  }
+  notRequired(): BooleanSchema<Maybe<TType>, TContext, TDefault, TFlags> {
+    return super.notRequired();
+  }
+  nullable(): BooleanSchema<TType | null, TContext, TDefault, TFlags> {
+    return super.nullable();
+  }
+  nonNullable(
+    msg?: Message,
+  ): BooleanSchema<NotNull<TType>, TContext, TDefault, TFlags> {
+    return super.nonNullable(msg);
+  }
 
-  nullable(msg?: Message): BooleanSchema<TType | null, TConfig>;
-  nonNullable(): BooleanSchema<NotNull<TType>, TConfig>;
+  strip(): BooleanSchema<TType, TContext, TDefault, SetFlag<TFlags, 's'>> {
+    return super.strip();
+  }
 }
+
+create.prototype = BooleanSchema.prototype;
diff --git a/src/date.ts b/src/date.ts
index e56bb8a..6348286 100644
--- a/src/date.ts
+++ b/src/date.ts
@@ -5,11 +5,13 @@ import isAbsent from './util/isAbsent';
 import Ref from './Reference';
 import type { AnyObject, Maybe, Message } from './types';
 import type {
-  Config,
   Defined,
+  Flags,
   NotNull,
+  SetFlag,
   Thunk,
   ToggleDefault,
+  UnsetFlag,
 } from './util/types';
 import BaseSchema from './schema';
 
@@ -21,7 +23,7 @@ let isDate = (obj: any): obj is Date =>
 export function create(): DateSchema;
 export function create<T extends Date, TContext = AnyObject>(): DateSchema<
   T | undefined,
-  Config<TContext>
+  TContext
 >;
 export function create() {
   return new DateSchema();
@@ -29,8 +31,10 @@ export function create() {
 
 export default class DateSchema<
   TType extends Maybe<Date> = Date | undefined,
-  TConfig extends Config<any, any> = Config
-> extends BaseSchema<TType, TConfig> {
+  TContext = AnyObject,
+  TDefault = undefined,
+  TFlags extends Flags = '',
+> extends BaseSchema<TType, TContext, TDefault, TFlags> {
   static INVALID_DATE = invalidDate;
 
   constructor() {
@@ -105,20 +109,33 @@ create.INVALID_DATE = invalidDate;
 
 export default interface DateSchema<
   TType extends Maybe<Date>,
-  TConfig extends Config<any, any> = Config
-> extends BaseSchema<TType, TConfig> {
+  TContext = AnyObject,
+  TDefault = undefined,
+  TFlags extends Flags = '',
+> extends BaseSchema<TType, TContext, TDefault, TFlags> {
   default<D extends Maybe<TType>>(
     def: Thunk<D>,
-  ): DateSchema<TType, ToggleDefault<TConfig, D>>;
+  ): DateSchema<TType, TContext, D, ToggleDefault<TFlags, D>>;
 
   concat<TOther extends DateSchema<any, any>>(schema: TOther): TOther;
 
-  defined(msg?: Message): DateSchema<Defined<TType>, TConfig>;
-  optional(): DateSchema<TType | undefined, TConfig>;
-
-  required(msg?: Message): DateSchema<NonNullable<TType>, TConfig>;
-  notRequired(): DateSchema<Maybe<TType>, TConfig>;
-
-  nullable(msg?: Message): DateSchema<TType | null, TConfig>;
-  nonNullable(): DateSchema<NotNull<TType>, TConfig>;
+  defined(
+    msg?: Message,
+  ): DateSchema<Defined<TType>, TContext, TDefault, TFlags>;
+  optional(): DateSchema<TType | undefined, TContext, TDefault, TFlags>;
+
+  required(
+    msg?: Message,
+  ): DateSchema<NonNullable<TType>, TContext, TDefault, TFlags>;
+  notRequired(): DateSchema<Maybe<TType>, TContext, TDefault, TFlags>;
+
+  nullable(msg?: Message): DateSchema<TType | null, TContext, TDefault, TFlags>;
+  nonNullable(): DateSchema<NotNull<TType>, TContext, TDefault, TFlags>;
+
+  strip(
+    enabled: false,
+  ): DateSchema<TType, TContext, TDefault, UnsetFlag<TFlags, 's'>>;
+  strip(
+    enabled?: true,
+  ): DateSchema<TType, TContext, TDefault, SetFlag<TFlags, 's'>>;
 }
diff --git a/src/index.ts b/src/index.ts
index ebd969d..e78a0a1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,4 @@
-import MixedSchema, { create as mixedCreate } from './mixed';
+import Mixed, { create as mixedCreate, MixedSchema } from './mixed';
 import BooleanSchema, { create as boolCreate } from './boolean';
 import StringSchema, { create as stringCreate } from './string';
 import NumberSchema, { create as numberCreate } from './number';
@@ -6,14 +6,13 @@ import DateSchema, { create as dateCreate } from './date';
 import ObjectSchema, { AnyObject, create as objectCreate } from './object';
 import ArraySchema, { create as arrayCreate } from './array';
 import { create as refCreate } from './Reference';
-import Lazy, { create as lazyCreate } from './Lazy';
+import { create as lazyCreate } from './Lazy';
 import ValidationError from './ValidationError';
 import reach from './util/reach';
 import isSchema from './util/isSchema';
 import setLocale from './setLocale';
 import BaseSchema, { AnySchema } from './schema';
-import type { TypeOf, Asserts, Config } from './util/types';
-import { Maybe } from './types';
+import type { InferType } from './util/types';
 
 function addMethod<T extends AnySchema>(
   schemaType: (...arg: any[]) => T,
@@ -37,34 +36,9 @@ function addMethod(schemaType: any, name: string, fn: any) {
   schemaType.prototype[name] = fn;
 }
 
-// type ObjectSchemaOf<T extends AnyObject, CustomTypes = never> = ObjectSchema<{
-//   [k in keyof T]-?:
-//     | SchemaOf<T[k], CustomTypes>
-//     | Lazy<SchemaOf<T[k], CustomTypes>>;
-// }>;
+export type AnyObjectSchema = ObjectSchema<any, any, any, any>;
 
-type SchemaOf<T, CustomTypes = never> = [T] extends [Array<infer E>]
-  ? ArraySchema<SchemaOf<E, CustomTypes> | Lazy<SchemaOf<E, CustomTypes>>>
-  : [T] extends [Maybe<string>]
-  ? StringSchema<T>
-  : [T] extends [Maybe<number>]
-  ? NumberSchema<T>
-  : T extends Date
-  ? DateSchema<T>
-  : T extends CustomTypes
-  ? BaseSchema<T, Config>
-  : [T] extends [AnyObject]
-  ? ObjectSchema<{
-      [k in keyof T]-?:
-        | SchemaOf<T[k], CustomTypes>
-        | Lazy<SchemaOf<T[k], CustomTypes>>;
-    }>
-  : //ObjectSchemaOf<T, CustomTypes>
-    never;
-
-export type AnyObjectSchema = ObjectSchema<any, any, any>;
-
-export type { SchemaOf, TypeOf, Asserts, Asserts as InferType, AnySchema };
+export type { AnyObject, InferType, InferType as Asserts, AnySchema };
 
 export {
   mixedCreate as mixed,
@@ -86,7 +60,8 @@ export {
 
 export {
   BaseSchema,
-  MixedSchema,
+  Mixed as MixedSchema,
+  MixedSchema as MixedSchemaClass,
   BooleanSchema,
   StringSchema,
   NumberSchema,
diff --git a/src/mixed.ts b/src/mixed.ts
index 1386de9..4b2c317 100644
--- a/src/mixed.ts
+++ b/src/mixed.ts
@@ -1,31 +1,59 @@
-import { Maybe, Message, Optionals } from './types';
-import type { Config, Defined, Thunk, ToggleDefault } from './util/types';
+import { AnyObject, Maybe, Message, Optionals } from './types';
+import type {
+  Defined,
+  Flags,
+  SetFlag,
+  Thunk,
+  ToggleDefault,
+  UnsetFlag,
+} from './util/types';
 import BaseSchema from './schema';
 
 export declare class MixedSchema<
   TType = any,
-  TConfig extends Config<any, any> = Config
-> extends BaseSchema<TType, TConfig> {
+  TContext = AnyObject,
+  TDefault = undefined,
+  TFlags extends Flags = '',
+> extends BaseSchema<TType, TContext, TDefault, TFlags> {
   default<D extends Maybe<TType>>(
     def: Thunk<D>,
-  ): MixedSchema<TType, ToggleDefault<TConfig, D>>;
-
-  concat<IT, IC extends Config<any, any>>(
-    schema: MixedSchema<IT, IC>,
-  ): MixedSchema<NonNullable<TType> | IT, TConfig & IC>;
-  concat<IT, IC extends Config<any, any>>(
-    schema: BaseSchema<IT, IC>,
-  ): MixedSchema<NonNullable<TType> | Optionals<IT>, TConfig & IC>;
+  ): MixedSchema<TType, TContext, D, ToggleDefault<TFlags, D>>;
+
+  concat<IT, IC, ID, IF extends Flags>(
+    schema: MixedSchema<IT, IC, ID, IF>,
+  ): MixedSchema<NonNullable<TType> | IT, TContext & IC, ID, TFlags | IF>;
+  concat<IT, IC, ID, IF extends Flags>(
+    schema: BaseSchema<IT, IC, ID, IF>,
+  ): MixedSchema<
+    NonNullable<TType> | Optionals<IT>,
+    TContext & IC,
+    ID,
+    TFlags | IF
+  >;
   concat(schema: this): this;
 
-  defined(msg?: Message): MixedSchema<Defined<TType>, TConfig>;
-  optional(): MixedSchema<TType | undefined, TConfig>;
+  defined(
+    msg?: Message,
+  ): MixedSchema<Defined<TType>, TContext, TDefault, TFlags>;
+  optional(): MixedSchema<TType | undefined, TContext, TDefault, TFlags>;
+
+  required(
+    msg?: Message,
+  ): MixedSchema<NonNullable<TType>, TContext, TDefault, TFlags>;
+  notRequired(): MixedSchema<Maybe<TType>, TContext, TDefault, TFlags>;
+
+  nullable(
+    msg?: Message,
+  ): MixedSchema<TType | null, TContext, TDefault, TFlags>;
 
-  required(msg?: Message): MixedSchema<NonNullable<TType>, TConfig>;
-  notRequired(): MixedSchema<Maybe<TType>, TConfig>;
+  nonNullable(): MixedSchema<Exclude<TType, null>, TContext, TDefault, TFlags>;
 
-  nullable(msg?: Message): MixedSchema<TType | null, TConfig>;
-  nonNullable(): MixedSchema<Exclude<TType, null>, TConfig>;
+  strip(
+    enabled: false,
+  ): MixedSchema<TType, TContext, TDefault, UnsetFlag<TFlags, 's'>>;
+  strip(
+    enabled?: true,
+  ): MixedSchema<TType, TContext, TDefault, SetFlag<TFlags, 's'>>;
 }
 
 const Mixed: typeof MixedSchema = BaseSchema as any;
diff --git a/src/number.ts b/src/number.ts
index f9bbf59..fda5986 100644
--- a/src/number.ts
+++ b/src/number.ts
@@ -1,16 +1,16 @@
 import { number as locale } from './locale';
 import isAbsent from './util/isAbsent';
-import type { AnyObject, Maybe } from './types';
+import type { AnyObject, Maybe, Message } from './types';
 import type Reference from './Reference';
 import type {
-  AnyConfig,
-  Config,
+  Concat,
   Defined,
-  MergeConfig,
+  Flags,
   NotNull,
   SetFlag,
   Thunk,
   ToggleDefault,
+  UnsetFlag,
 } from './util/types';
 import BaseSchema from './schema';
 
@@ -19,7 +19,7 @@ let isNaN = (value: Maybe<number>) => value != +value!;
 export function create(): NumberSchema;
 export function create<T extends number, TContext = AnyObject>(): NumberSchema<
   T | undefined,
-  Config<TContext>
+  TContext
 >;
 export function create() {
   return new NumberSchema();
@@ -27,8 +27,10 @@ export function create() {
 
 export default class NumberSchema<
   TType extends Maybe<number> = number | undefined,
-  TConfig extends Config<any, any> = Config
-> extends BaseSchema<TType, TConfig> {
+  TContext = AnyObject,
+  TDefault = undefined,
+  TFlags extends Flags = '',
+> extends BaseSchema<TType, TContext, TDefault, TFlags> {
   constructor() {
     super({ type: 'number' });
 
@@ -124,20 +126,20 @@ export default class NumberSchema<
     return this.transform((value) => (!isAbsent(value) ? value | 0 : value));
   }
 
-  round(method: 'ceil' | 'floor' | 'round' | 'trunc') {
+  round(method?: 'ceil' | 'floor' | 'round' | 'trunc') {
     let avail = ['ceil', 'floor', 'round', 'trunc'];
     method = (method?.toLowerCase() as any) || ('round' as const);
 
     // this exists for symemtry with the new Math.trunc
     if (method === 'trunc') return this.truncate();
 
-    if (avail.indexOf(method.toLowerCase()) === -1)
+    if (avail.indexOf(method!.toLowerCase()) === -1)
       throw new TypeError(
         'Only valid options for round() are: ' + avail.join(', '),
       );
 
     return this.transform((value) =>
-      !isAbsent(value) ? Math[method](value) : value,
+      !isAbsent(value) ? Math[method!](value) : value,
     );
   }
 }
@@ -150,25 +152,43 @@ create.prototype = NumberSchema.prototype;
 
 export default interface NumberSchema<
   TType extends Maybe<number> = number | undefined,
-  TConfig extends Config<any, any> = Config
-> extends BaseSchema<TType, TConfig> {
-  strip(): NumberSchema<TType, SetFlag<TConfig, 's'>>;
-
+  TContext = AnyObject,
+  TDefault = undefined,
+  TFlags extends Flags = '',
+> extends BaseSchema<TType, TContext, TDefault, TFlags> {
   default<D extends Maybe<TType>>(
     def: Thunk<D>,
-  ): NumberSchema<TType, ToggleDefault<TConfig, D>>;
-
-  concat<T extends Maybe<number>, C extends AnyConfig>(
-    schema: NumberSchema<T, C>,
-  ): NumberSchema<NonNullable<TType> | T, MergeConfig<TConfig, C>>;
+  ): NumberSchema<TType, TContext, D, ToggleDefault<TFlags, D>>;
+
+  concat<UType extends Maybe<number>, UContext, UFlags extends Flags, UDefault>(
+    schema: NumberSchema<UType, UContext, UDefault, UFlags>,
+  ): NumberSchema<
+    Concat<TType, UType>,
+    TContext & UContext,
+    UDefault,
+    TFlags | UFlags
+  >;
   concat(schema: this): this;
 
-  defined(msg?: Message): NumberSchema<Defined<TType>, TConfig>;
-  optional(): NumberSchema<TType | undefined, TConfig>;
-
-  required(msg?: Message): NumberSchema<NonNullable<TType>, TConfig>;
-  notRequired(): NumberSchema<Maybe<TType>, TConfig>;
-
-  nullable(msg?: Message): NumberSchema<TType | null, TConfig>;
-  nonNullable(): NumberSchema<NotNull<TType>, TConfig>;
+  defined(
+    msg?: Message,
+  ): NumberSchema<Defined<TType>, TContext, TDefault, TFlags>;
+  optional(): NumberSchema<TType | undefined, TContext, TDefault, TFlags>;
+
+  required(
+    msg?: Message,
+  ): NumberSchema<NonNullable<TType>, TContext, TDefault, TFlags>;
+  notRequired(): NumberSchema<Maybe<TType>, TContext, TDefault, TFlags>;
+
+  nullable(
+    msg?: Message,
+  ): NumberSchema<TType | null, TContext, TDefault, TFlags>;
+  nonNullable(): NumberSchema<NotNull<TType>, TContext, TDefault, TFlags>;
+
+  strip(
+    enabled: false,
+  ): NumberSchema<TType, TContext, TDefault, UnsetFlag<TFlags, 's'>>;
+  strip(
+    enabled?: true,
+  ): NumberSchema<TType, TContext, TDefault, SetFlag<TFlags, 's'>>;
 }
diff --git a/src/object.ts b/src/object.ts
index edb9e21..e7ab7f7 100644
--- a/src/object.ts
+++ b/src/object.ts
@@ -2,89 +2,40 @@
 import { getter, normalizePath, join } from 'property-expr';
 import { camelCase, snakeCase } from 'tiny-case';
 
+import {
+  Flags,
+  ISchema,
+  SetFlag,
+  ToggleDefault,
+  UnsetFlag,
+} from './util/types';
+
 import { object as locale } from './locale';
 import sortFields from './util/sortFields';
 import sortByKeyOrder from './util/sortByKeyOrder';
 import runTests from './util/runTests';
-import {
-  InternalOptions,
-  Callback,
-  Maybe,
-  Optionals,
-  Preserve,
-  Message,
-} from './types';
+import { InternalOptions, Callback, Maybe, Message } from './types';
 import ValidationError from './ValidationError';
-import type {
-  TypedSchema,
-  Defined,
-  Thunk,
-  Config,
-  NotNull,
-  ToggleDefault,
-  _,
-  MakePartial,
-} from './util/types';
+import type { Defined, Thunk, NotNull, _ } from './util/types';
 import type Reference from './Reference';
-import Lazy from './Lazy';
-import BaseSchema, {
-  AnySchema,
-  ResolveFlags,
-  SchemaObjectDescription,
-  SchemaSpec,
-} from './schema';
+import BaseSchema, { SchemaObjectDescription, SchemaSpec } from './schema';
 import { ResolveOptions } from './Condition';
+import type {
+  AnyObject,
+  DefaultFromShape,
+  MakePartial,
+  MergeObjectTypes,
+  ObjectShape,
+  PartialDeep,
+  TypeFromShape,
+} from './util/objectTypes';
 
-export type Assign<T extends {}, U extends {}> = {
-  [P in keyof T]: P extends keyof U ? U[P] : T[P];
-} & U;
-
-export type AnyObject = Record<string, any>;
-
-export type ObjectShape = Record<
-  string,
-  AnySchema | Reference | Lazy<any, any>
->;
-
-type FieldType<
-  T extends AnySchema | Reference | Lazy<any, any>,
-  F extends '__type' | '__outputType',
-> = T extends TypedSchema ? T[F] : T extends Reference ? unknown : never;
-
-export type DefaultFromShape<Shape extends ObjectShape> = {
-  [K in keyof Shape]: Shape[K] extends ObjectSchema<infer TShape>
-    ? DefaultFromShape<TShape>
-    : Shape[K] extends { getDefault: () => infer D }
-    ? Preserve<D, undefined> extends never
-      ? Defined<D>
-      : Preserve<D, undefined>
-    : undefined;
-};
-
-export type TypeOfShape<Shape extends ObjectShape> = {
-  [K in keyof Shape]: FieldType<Shape[K], '__type'>;
-};
-
-// type Strip<K, V> = V extends AnySchema
-//   ? HasFlag<V, 's'> extends never
-//     ? K
-//     : never
-//   : K;
-
-export type AssertsShape<S extends ObjectShape> = MakePartial<{
-  [K in keyof S]: FieldType<S[K], '__outputType'>;
-}> & { [k: string]: any };
+export type { AnyObject };
 
-export type PartialSchema<S extends ObjectShape> = {
-  [K in keyof S]: S[K] extends BaseSchema ? ReturnType<S[K]['optional']> : S[K];
-};
+type MakeKeysOptional<T> = T extends AnyObject ? _<MakePartial<T>> : T;
 
-export type DeepPartialSchema<S extends ObjectShape> = {
-  [K in keyof S]: S[K] extends ObjectSchema<any, any, any>
-    ? ReturnType<S[K]['deepPartial']>
-    : S[K] extends BaseSchema
-    ? ReturnType<S[K]['optional']>
-    : S[K];
+export type Shape<T extends Maybe<AnyObject>, C = AnyObject> = {
+  [field in keyof T]: ISchema<T[field], C> | Reference;
 };
 
 export type ObjectSchemaSpec = SchemaSpec<any> & {
@@ -109,23 +60,62 @@ function unknown(ctx: ObjectSchema<any, any, any>, value: any) {
 
 const defaultSort = sortByKeyOrder([]);
 
+export function create<C = AnyObject, S extends ObjectShape = {}>(spec?: S) {
+  type TIn = _<TypeFromShape<S, C>>;
+  type TDefault = _<DefaultFromShape<S>>;
+
+  return new ObjectSchema<TIn, C, TDefault>(spec as any);
+}
+
+export default interface ObjectSchema<
+  TIn extends Maybe<AnyObject>,
+  TContext = AnyObject,
+  // important that this is `any` so that using `ObjectSchema<MyType>`'s default
+  // will match object schema regardless of defaults
+  TDefault = any,
+  TFlags extends Flags = 'd',
+> extends BaseSchema<MakeKeysOptional<TIn>, TContext, TDefault, TFlags> {
+  default<D extends Maybe<AnyObject>>(
+    def: Thunk<D>,
+  ): ObjectSchema<TIn, TContext, D, ToggleDefault<TFlags, 'd'>>;
+
+  defined(
+    msg?: Message,
+  ): ObjectSchema<Defined<TIn>, TContext, TDefault, TFlags>;
+  optional(): ObjectSchema<TIn | undefined, TContext, TDefault, TFlags>;
+
+  required(
+    msg?: Message,
+  ): ObjectSchema<NonNullable<TIn>, TContext, TDefault, TFlags>;
+  notRequired(): ObjectSchema<Maybe<TIn>, TContext, TDefault, TFlags>;
+
+  nullable(msg?: Message): ObjectSchema<TIn | null, TContext, TDefault, TFlags>;
+  nonNullable(): ObjectSchema<NotNull<TIn>, TContext, TDefault, TFlags>;
+
+  strip(
+    enabled: false,
+  ): ObjectSchema<TIn, TContext, TDefault, UnsetFlag<TFlags, 's'>>;
+  strip(
+    enabled?: true,
+  ): ObjectSchema<TIn, TContext, TDefault, SetFlag<TFlags, 's'>>;
+}
+
 export default class ObjectSchema<
-  TShape extends ObjectShape,
-  TConfig extends Config<any, any> = Config<AnyObject, 'd'>,
-  TIn extends Maybe<AssertsShape<TShape>> = AssertsShape<TShape> | undefined,
-> extends BaseSchema<TIn, TConfig> {
-  fields: TShape = Object.create(null);
+  TIn extends Maybe<AnyObject>,
+  TContext = AnyObject,
+  TDefault = any,
+  TFlags extends Flags = 'd',
+> extends BaseSchema<MakeKeysOptional<TIn>, TContext, TDefault, TFlags> {
+  fields: Shape<NonNullable<TIn>, TContext> = Object.create(null);
 
   declare spec: ObjectSchemaSpec;
 
-  declare readonly __outputType: ResolveFlags<_<TIn>, TConfig['flags']>;
-
   private _sortErrors = defaultSort;
-  private _nodes: readonly string[] = [];
+  private _nodes: string[] = []; //readonly (keyof TIn & string)[]
 
   private _excludedEdges: readonly [nodeA: string, nodeB: string][] = [];
 
-  constructor(spec?: TShape) {
+  constructor(spec?: Shape<TIn, TContext>) {
     super({
       type: 'object',
     });
@@ -144,19 +134,18 @@ export default class ObjectSchema<
       });
 
       if (spec) {
-        this.shape(spec);
+        this.shape(spec as any);
       }
     });
   }
 
-  protected _typeCheck(value: any): value is NonNullable<TIn> {
+  protected _typeCheck(
+    value: any,
+  ): value is NonNullable<MakeKeysOptional<TIn>> {
     return isObject(value) || typeof value === 'function';
   }
 
-  protected _cast(
-    _value: any,
-    options: InternalOptions<TConfig['context']> = {},
-  ) {
+  protected _cast(_value: any, options: InternalOptions<TContext> = {}) {
     let value = super._cast(_value, options);
 
     //should ignore nulls here
@@ -167,12 +156,13 @@ export default class ObjectSchema<
     let fields = this.fields;
 
     let strip = options.stripUnknown ?? this.spec.noUnknown;
-    let props = this._nodes.concat(
-      Object.keys(value).filter((v) => this._nodes.indexOf(v) === -1),
+    let props = ([] as string[]).concat(
+      this._nodes,
+      Object.keys(value).filter((v) => !this._nodes.includes(v)),
     );
 
     let intermediateValue: Record<string, unknown> = {}; // is filled during the transform below
-    let innerOptions: InternalOptions = {
+    let innerOptions: InternalOptions<TContext> = {
       ...options,
       parent: intermediateValue,
       __validating: options.__validating || false,
@@ -228,7 +218,7 @@ export default class ObjectSchema<
 
   protected _validate(
     _value: any,
-    opts: InternalOptions<TConfig['context']> = {},
+    opts: InternalOptions<TContext> = {},
     callback: Callback,
   ) {
     let errors = [] as ValidationError[];
@@ -319,15 +309,14 @@ export default class ObjectSchema<
     return next;
   }
 
-  concat<TOther extends ObjectSchema<any, any, any>>(
-    schema: TOther,
-  ): TOther extends ObjectSchema<infer S, infer C, infer IType>
-    ? ObjectSchema<
-        TShape & S,
-        TConfig & C,
-        AssertsShape<TShape & S> | Optionals<IType>
-      >
-    : never;
+  concat<IIn, IC, ID, IF extends Flags>(
+    schema: ObjectSchema<IIn, IC, ID, IF>,
+  ): ObjectSchema<
+    NonNullable<TIn> | IIn,
+    TContext & IC,
+    TDefault & ID,
+    TFlags | IF
+  >;
   concat(schema: this): this;
   concat(schema: any): any {
     let next = super.concat(schema) as any;
@@ -359,22 +348,20 @@ export default class ObjectSchema<
     if (!this._nodes.length) {
       return undefined;
     }
-    return this.getDefaultFromShape();
-  }
 
-  getDefaultFromShape(): _<DefaultFromShape<TShape>> {
-    let dft = {} as Record<string, unknown>;
+    let dft: any = {};
     this._nodes.forEach((key) => {
-      const field = this.fields[key];
-      dft[key] = 'default' in field ? field.getDefault() : undefined;
+      const field = this.fields[key] as any;
+      dft[key] = 'getDefault' in field ? field.getDefault() : undefined;
     });
-    return dft as any;
+
+    return dft;
   }
 
-  private setFields<S extends ObjectShape>(
-    shape: S,
+  private setFields<TInNext extends Maybe<AnyObject>, TDefaultNext>(
+    shape: Shape<TInNext, TContext>,
     excludedEdges?: readonly [string, string][],
-  ): ObjectSchema<S, TConfig, AssertsShape<S> | Optionals<TIn>> {
+  ): ObjectSchema<TInNext, TContext, TDefaultNext, TFlags> {
     let next = this.clone() as any;
     next.fields = shape;
 
@@ -385,10 +372,16 @@ export default class ObjectSchema<
     return next;
   }
 
-  shape<TNextShape extends ObjectShape>(
-    additions: TNextShape,
+  shape<U extends ObjectShape>(
+    additions: U,
     excludes: [string, string][] = [],
   ) {
+    type UIn = TypeFromShape<U, TContext>;
+    type UDefault = Extract<TFlags, 'd'> extends never
+      ? // not defaulted then assume the default is derived and should be merged
+        _<TDefault & DefaultFromShape<U>>
+      : TDefault;
+
     return this.clone().withMutation((next) => {
       let edges = next._excludedEdges;
       if (excludes.length) {
@@ -398,8 +391,8 @@ export default class ObjectSchema<
       }
 
       // XXX: excludes here is wrong
-      return next.setFields(
-        Object.assign(next.fields, additions) as Assign<TShape, TNextShape>,
+      return next.setFields<_<MergeObjectTypes<TIn, UIn>>, UDefault>(
+        Object.assign(next.fields, additions) as any,
         edges,
       );
     });
@@ -411,14 +404,10 @@ export default class ObjectSchema<
       partial[key] = schema instanceof BaseSchema ? schema.optional() : schema;
     }
 
-    return this.setFields(partial as PartialSchema<TShape>);
+    return this.setFields<Partial<TIn>, TDefault>(partial);
   }
 
-  deepPartial(): ObjectSchema<
-    DeepPartialSchema<TShape>,
-    TConfig,
-    Optionals<TIn> | undefined | AssertsShape<DeepPartialSchema<TShape>>
-  > {
+  deepPartial() {
     const partial: any = {};
     for (const [key, schema] of Object.entries(this.fields)) {
       if (schema instanceof ObjectSchema) partial[key] = schema.deepPartial();
@@ -426,30 +415,29 @@ export default class ObjectSchema<
         partial[key] =
           schema instanceof BaseSchema ? schema.optional() : schema;
     }
-
-    return this.setFields(partial as DeepPartialSchema<TShape>);
+    return this.setFields<PartialDeep<TIn>, TDefault>(partial);
   }
 
-  pick<TKey extends keyof TShape>(keys: TKey[]) {
+  pick<TKey extends keyof TIn>(keys: TKey[]) {
     const picked: any = {};
     for (const key of keys) {
       if (this.fields[key]) picked[key] = this.fields[key];
     }
 
-    return this.setFields(picked as Pick<TShape, TKey>);
+    return this.setFields<{ [K in TKey]: TIn[K] }, TDefault>(picked);
   }
 
-  omit<TKey extends keyof TShape>(keys: TKey[]) {
+  omit<TKey extends keyof TIn>(keys: TKey[]) {
     const fields = { ...this.fields };
 
     for (const key of keys) {
       delete fields[key];
     }
 
-    return this.setFields(fields as Omit<TShape, TKey>);
+    return this.setFields<Omit<TIn, TKey>, TDefault>(fields);
   }
 
-  from(from: string, to: keyof TShape, alias?: boolean) {
+  from(from: string, to: keyof TIn, alias?: boolean) {
     let fromGetter = getter(from, true);
 
     return this.transform((obj) => {
@@ -466,8 +454,10 @@ export default class ObjectSchema<
     });
   }
 
-  noUnknown(noAllow = true, message = locale.noUnknown) {
-    if (typeof noAllow === 'string') {
+  noUnknown(message?: Message): this;
+  noUnknown(noAllow: boolean, message?: Message): this;
+  noUnknown(noAllow: Message | boolean = true, message = locale.noUnknown) {
+    if (typeof noAllow !== 'boolean') {
       message = noAllow;
       noAllow = true;
     }
@@ -517,7 +507,7 @@ export default class ObjectSchema<
     return this.transformKeys((key) => snakeCase(key).toUpperCase());
   }
 
-  describe(options?: ResolveOptions<TConfig['context']>) {
+  describe(options?: ResolveOptions<TContext>) {
     let base = super.describe(options) as SchemaObjectDescription;
     base.fields = {};
     for (const [key, value] of Object.entries(this.fields)) {
@@ -535,27 +525,4 @@ export default class ObjectSchema<
   }
 }
 
-export function create<TShape extends ObjectShape = {}>(spec?: TShape) {
-  return new ObjectSchema<TShape>(spec);
-}
-
 create.prototype = ObjectSchema.prototype;
-
-export default interface ObjectSchema<
-  TShape extends ObjectShape,
-  TConfig extends Config<any, any> = Config<AnyObject, 'd'>,
-  TIn extends Maybe<AssertsShape<TShape>> = AssertsShape<TShape> | undefined,
-> extends BaseSchema<TIn, TConfig> {
-  default<D extends Maybe<AnyObject>>(
-    def: Thunk<D>,
-  ): ObjectSchema<TShape, ToggleDefault<TConfig, D>, TIn>;
-
-  defined(msg?: Message): ObjectSchema<TShape, TConfig, Defined<TIn>>;
-  optional(): ObjectSchema<TShape, TConfig, TIn | undefined>;
-
-  required(msg?: Message): ObjectSchema<TShape, TConfig, NonNullable<TIn>>;
-  notRequired(): ObjectSchema<TShape, TConfig, Maybe<TIn>>;
-
-  nullable(isNullable?: true): ObjectSchema<TShape, TConfig, TIn | null>;
-  nullable(isNullable: false): ObjectSchema<TShape, TConfig, NotNull<TIn>>;
-}
diff --git a/src/schema.ts b/src/schema.ts
index d6071d7..fc1cbe6 100644
--- a/src/schema.ts
+++ b/src/schema.ts
@@ -20,30 +20,23 @@ import {
   InternalOptions,
   Maybe,
   ExtraParams,
-  Preserve,
+  AnyObject,
 } from './types';
 
 import ValidationError from './ValidationError';
 import ReferenceSet from './util/ReferenceSet';
 import Reference from './Reference';
 import isAbsent from './util/isAbsent';
-import type { Config, Defined, Flags, SetFlag, Thunk, _ } from './util/types';
+import type {
+  Defined,
+  Flags,
+  ISchema,
+  ResolveFlags,
+  Thunk,
+  _,
+} from './util/types';
 import toArray from './util/toArray';
 
-export type { Config };
-
-export type ConfigOf<T> = T extends AnySchema<any, infer C> ? C : never;
-
-export type ContextOf<T> = ConfigOf<T>['context'];
-
-export type FlagsOf<T> = T extends AnySchema ? T['__flags'] : never;
-
-export type HasFlag<T, F extends Flags> = F extends FlagsOf<T> ? true : never;
-
-export type ResolveFlags<T, F extends Flags> = Preserve<F, 'd'> extends never
-  ? T
-  : Defined<T>;
-
 export type SchemaSpec<TDefault> = {
   nullable: boolean;
   optional: boolean;
@@ -61,10 +54,12 @@ export type SchemaOptions<TDefault> = {
   spec?: SchemaSpec<TDefault>;
 };
 
-export type AnySchema<TType = any, C extends Config = any> = BaseSchema<
-  TType,
-  C
->;
+export type AnySchema<
+  TType = any,
+  C = AnyObject,
+  D = any,
+  F extends Flags = Flags,
+> = BaseSchema<TType, C, D, F>;
 
 export interface CastOptions<C = {}> {
   parent?: any;
@@ -121,15 +116,18 @@ export interface SchemaDescription {
 
 export default abstract class BaseSchema<
   TType = any,
-  TConfig extends Config<any, any> = Config,
-> {
-  declare readonly type: string;
-
-  declare readonly __type: TType;
-  declare readonly __outputType: ResolveFlags<TType, TConfig['flags']>;
-
-  declare readonly __flags: TConfig['flags'];
+  TContext = AnyObject,
+  TDefault = any,
+  TFlags extends Flags = '',
+> implements ISchema<TType, TContext, TFlags, TDefault>
+{
+  readonly type: string;
+
+  declare readonly __outputType: ResolveFlags<TType, TFlags>;
+  declare readonly __context: TContext;
+  declare readonly __flags: TFlags;
   declare readonly __isYupSchema__: boolean;
+  declare readonly __default: TDefault;
 
   readonly deps: readonly string[] = [];
 
@@ -227,14 +225,6 @@ export default abstract class BaseSchema<
     return next;
   }
 
-  // withContext<C extends AnyObject>(): BaseSchema<
-  //   TType,
-  //   C,
-  //   TOut
-  // > {
-  //   return this as any;
-  // }
-
   withMutation<T>(fn: (schema: this) => T): T {
     let before = this._mutate;
     this._mutate = true;
@@ -258,12 +248,6 @@ export default abstract class BaseSchema<
 
     const mergedSpec = { ...base.spec, ...combined.spec };
 
-    // if (combined.spec.nullable === UNSET)
-    //   mergedSpec.nullable = base.spec.nullable;
-
-    // if (combined.spec.presence === UNSET)
-    //   mergedSpec.presence = base.spec.presence;
-
     combined.spec = mergedSpec;
     combined.internalTests = {
       ...base.internalTests,
@@ -307,7 +291,7 @@ export default abstract class BaseSchema<
     return this._typeCheck(v);
   }
 
-  resolve(options: ResolveOptions) {
+  resolve(options: ResolveOptions<TContext>) {
     let schema = this;
 
     if (schema.conditions.length) {
@@ -318,7 +302,7 @@ export default abstract class BaseSchema<
       schema = conditions.reduce(
         (prevSchema, condition) => condition.resolve(prevSchema, options),
         schema,
-      ) as this;
+      ) as any as this;
 
       schema = schema.resolve(options);
     }
@@ -333,10 +317,7 @@ export default abstract class BaseSchema<
    * @param {*=} options.parent
    * @param {*=} options.context
    */
-  cast(
-    value: any,
-    options: CastOptions<TConfig['context']> = {},
-  ): this['__outputType'] {
+  cast(value: any, options: CastOptions<TContext> = {}): this['__outputType'] {
     let resolvedSchema = this.resolve({
       value,
       ...options,
@@ -364,10 +345,7 @@ export default abstract class BaseSchema<
     return result;
   }
 
-  protected _cast(
-    rawValue: any,
-    _options: CastOptions<TConfig['context']>,
-  ): any {
+  protected _cast(rawValue: any, _options: CastOptions<TContext>): any {
     let value =
       rawValue === undefined
         ? rawValue
@@ -385,7 +363,7 @@ export default abstract class BaseSchema<
 
   protected _validate(
     _value: any,
-    options: InternalOptions<TConfig['context']> = {},
+    options: InternalOptions<TContext> = {},
     cb: Callback,
   ): void {
     let {
@@ -447,14 +425,14 @@ export default abstract class BaseSchema<
     );
   }
 
-  // validate<U extends TType>(value: U, options?: ValidateOptions<TConfig['context']>): Promise<U>;
+  // validate<U extends TType>(value: U, options?: ValidateOptions<TContext>): Promise<U>;
   validate(
     value: any,
-    options?: ValidateOptions<TConfig['context']>,
+    options?: ValidateOptions<TContext>,
   ): Promise<this['__outputType']>;
   validate(
     value: any,
-    options?: ValidateOptions<TConfig['context']>,
+    options?: ValidateOptions<TContext>,
     maybeCb?: Callback,
   ) {
     let schema = this.resolve({ ...options, value });
@@ -470,14 +448,14 @@ export default abstract class BaseSchema<
         );
   }
 
-  // validateSync<U extends TType>(value: U, options?: ValidateOptions<TConfig['context']>): U;
+  // validateSync<U extends TType>(value: U, options?: ValidateOptions<TContext>): U;
   validateSync(
     value: any,
-    options?: ValidateOptions<TConfig['context']>,
+    options?: ValidateOptions<TContext>,
   ): this['__outputType'];
   validateSync(
     value: any,
-    options?: ValidateOptions<TConfig['context']>,
+    options?: ValidateOptions<TContext>,
   ): this['__outputType'] {
     let schema = this.resolve({ ...options, value });
     let result: any;
@@ -490,10 +468,7 @@ export default abstract class BaseSchema<
     return result;
   }
 
-  isValid(
-    value: any,
-    options?: ValidateOptions<TConfig['context']>,
-  ): Promise<boolean> {
+  isValid(value: any, options?: ValidateOptions<TContext>): Promise<boolean> {
     return this.validate(value, options).then(
       () => true,
       (err) => {
@@ -505,7 +480,7 @@ export default abstract class BaseSchema<
 
   isValidSync(
     value: any,
-    options?: ValidateOptions<TConfig['context']>,
+    options?: ValidateOptions<TContext>,
   ): value is this['__outputType'] {
     try {
       this.validateSync(value, options);
@@ -529,11 +504,9 @@ export default abstract class BaseSchema<
   }
 
   getDefault(
-    options?: ResolveOptions,
+    options?: ResolveOptions<TContext>,
     // If schema is defaulted we know it's at least not undefined
-  ): Preserve<TConfig['flags'], 'd'> extends never
-    ? undefined
-    : Defined<TType> {
+  ): TDefault {
     let schema = this.resolve(options || {});
     return schema._getDefault();
   }
@@ -641,16 +614,13 @@ export default abstract class BaseSchema<
    * If an exclusive test is added to a schema with non-exclusive tests of the same name
    * the previous tests are removed and further tests of the same name will replace each other.
    */
-  test(options: TestConfig<this['__outputType'], TConfig['context']>): this;
-  test(test: TestFunction<this['__outputType'], TConfig['context']>): this;
-  test(
-    name: string,
-    test: TestFunction<this['__outputType'], TConfig['context']>,
-  ): this;
+  test(options: TestConfig<this['__outputType'], TContext>): this;
+  test(test: TestFunction<this['__outputType'], TContext>): this;
+  test(name: string, test: TestFunction<this['__outputType'], TContext>): this;
   test(
     name: string,
     message: Message,
-    test: TestFunction<this['__outputType'], TConfig['context']>,
+    test: TestFunction<this['__outputType'], TContext>,
   ): this;
   test(...args: any[]) {
     let opts: TestConfig;
@@ -806,7 +776,8 @@ export default abstract class BaseSchema<
     return next;
   }
 
-  strip(strip = true): BaseSchema<TType, SetFlag<TConfig, 's'>> {
+  //BaseSchema<TType, SetFlag<TFlags, 's'>>
+  strip(strip = true): any {
     let next = this.clone();
     next.spec.strip = strip;
     return next as any;
@@ -817,7 +788,7 @@ export default abstract class BaseSchema<
    *
    * @param options Provide any needed context for resolving runtime schema alterations (lazy, when conditions, etc).
    */
-  describe(options?: ResolveOptions<TConfig['context']>) {
+  describe(options?: ResolveOptions<TContext>) {
     const next = (options ? this.resolve(options) : this).clone();
     const { label, meta, optional, nullable } = next.spec;
     const description: SchemaDescription = {
@@ -840,19 +811,22 @@ export default abstract class BaseSchema<
 }
 
 export default interface BaseSchema<
-  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  /* eslint-disable @typescript-eslint/no-unused-vars */
   TType = any,
-  TConfig extends Config<any, any> = Config,
+  TContext = AnyObject,
+  TDefault = any,
+  TFlags extends Flags = '',
+  /* eslint-enable @typescript-eslint/no-unused-vars */
 > {
   validateAt(
     path: string,
     value: any,
-    options?: ValidateOptions<TConfig['context']>,
+    options?: ValidateOptions<TContext>,
   ): Promise<any>;
   validateSyncAt(
     path: string,
     value: any,
-    options?: ValidateOptions<TConfig['context']>,
+    options?: ValidateOptions<TContext>,
   ): any;
   equals: BaseSchema['oneOf'];
   is: BaseSchema['oneOf'];
diff --git a/src/string.ts b/src/string.ts
index ea2eb29..7b74df0 100644
--- a/src/string.ts
+++ b/src/string.ts
@@ -3,21 +3,28 @@ import isAbsent from './util/isAbsent';
 import type Reference from './Reference';
 import type { Message, Maybe, AnyObject, Optionals } from './types';
 import type {
-  AnyConfig,
+  Concat,
   Defined,
-  MergeConfig,
+  Flags,
   NotNull,
   SetFlag,
   Thunk,
   ToggleDefault,
+  UnsetFlag,
 } from './util/types';
-import BaseSchema, { Config } from './schema';
-// eslint-disable-next-line
-let rEmail = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
-// eslint-disable-next-line
-let rUrl = /^((https?|ftp):)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
+import BaseSchema from './schema';
+
+let rEmail =
+  // eslint-disable-next-line
+  /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
+
+let rUrl =
+  // eslint-disable-next-line
+  /^((https?|ftp):)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
+
 // eslint-disable-next-line
-let rUUID = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
+let rUUID =
+  /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
 
 let isTrimmed = (value: Maybe<string>) =>
   isAbsent(value) || value === value.trim();
@@ -33,7 +40,7 @@ let objStringTag = {}.toString();
 function create(): StringSchema;
 function create<T extends string, TContext = AnyObject>(): StringSchema<
   T | undefined,
-  Config<TContext>
+  TContext
 >;
 function create() {
   return new StringSchema();
@@ -43,8 +50,10 @@ export { create };
 
 export default class StringSchema<
   TType extends Maybe<string> = string | undefined,
-  TConfig extends AnyConfig = Config
-> extends BaseSchema<TType, TConfig> {
+  TContext = AnyObject,
+  TDefault = undefined,
+  TFlags extends Flags = '',
+> extends BaseSchema<TType, TContext, TDefault, TFlags> {
   constructor() {
     super({ type: 'string' });
 
@@ -218,29 +227,48 @@ create.prototype = StringSchema.prototype;
 
 export default interface StringSchema<
   TType extends Maybe<string> = string | undefined,
-  TConfig extends AnyConfig = Config
-> extends BaseSchema<TType, TConfig> {
+  TContext = AnyObject,
+  TDefault = undefined,
+  TFlags extends Flags = '',
+> extends BaseSchema<TType, TContext, TDefault, TFlags> {
   default<D extends Maybe<TType>>(
     def: Thunk<D>,
-  ): StringSchema<TType, ToggleDefault<TConfig, D>>;
+  ): StringSchema<TType, TContext, D, ToggleDefault<TFlags, D>>;
 
   oneOf<U extends TType>(
     arrayOfValues: ReadonlyArray<U | Reference>,
     message?: MixedLocale['oneOf'],
-  ): StringSchema<U | Optionals<TType>, TConfig>;
-
-  concat<T extends Maybe<string>, C extends AnyConfig>(
-    schema: StringSchema<T, C>,
-  ): StringSchema<NonNullable<TType> | T, MergeConfig<TConfig, C>>;
+  ): StringSchema<U | Optionals<TType>, TContext, TDefault, TFlags>;
+
+  concat<UType extends Maybe<string>, UContext, UDefault, UFlags extends Flags>(
+    schema: StringSchema<UType, UContext, UDefault, UFlags>,
+  ): StringSchema<
+    Concat<TType, UType>,
+    TContext & UContext,
+    UDefault,
+    TFlags | UFlags
+  >;
   concat(schema: this): this;
 
-  defined(msg?: Message): StringSchema<Defined<TType>, TConfig>;
-  optional(): StringSchema<TType | undefined, TConfig>;
-
-  required(msg?: Message): StringSchema<NonNullable<TType>, TConfig>;
-  notRequired(): StringSchema<Maybe<TType>, TConfig>;
-
-  nullable(msg?: Message<any>): StringSchema<TType | null, TConfig>;
-  nonNullable(): StringSchema<NotNull<TType>, TConfig>;
-  strip(): StringSchema<TType, SetFlag<TConfig, 's'>>;
+  defined(
+    msg?: Message,
+  ): StringSchema<Defined<TType>, TContext, TDefault, TFlags>;
+  optional(): StringSchema<TType | undefined, TContext, TDefault, TFlags>;
+
+  required(
+    msg?: Message,
+  ): StringSchema<NonNullable<TType>, TContext, TDefault, TFlags>;
+  notRequired(): StringSchema<Maybe<TType>, TContext, TDefault, TFlags>;
+
+  nullable(
+    msg?: Message<any>,
+  ): StringSchema<TType | null, TContext, TDefault, TFlags>;
+  nonNullable(): StringSchema<NotNull<TType>, TContext, TDefault, TFlags>;
+
+  strip(
+    enabled: false,
+  ): StringSchema<TType, TContext, TDefault, UnsetFlag<TFlags, 's'>>;
+  strip(
+    enabled?: true,
+  ): StringSchema<TType, TContext, TDefault, SetFlag<TFlags, 's'>>;
 }
diff --git a/src/types.ts b/src/types.ts
index 1e0f6b1..1020c21 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,9 +1,8 @@
 import type { AnySchema } from './schema';
-import type Lazy from './Lazy';
+import type { AnyObject } from './util/objectTypes';
+import type { ISchema } from './util/types';
 
-export type AnyObject = Record<string, any>;
-
-export type SchemaLike = AnySchema | Lazy<any>;
+export type { ISchema, AnyObject, AnySchema };
 
 export type Callback<T = any> = (err: Error | null, value?: T) => void;
 
@@ -44,7 +43,7 @@ export interface InternalOptions<TContext = {}>
   parent?: any;
   path?: string;
   sync?: boolean;
-  from?: { schema: AnySchema; value: any }[];
+  from?: { schema: ISchema<any, TContext>; value: any }[];
 }
 
 export interface MessageParams {
diff --git a/src/util/createValidation.ts b/src/util/createValidation.ts
index 8149428..99b2ee0 100644
--- a/src/util/createValidation.ts
+++ b/src/util/createValidation.ts
@@ -9,6 +9,7 @@ import {
 } from '../types';
 import Reference from '../Reference';
 import type { AnySchema } from '../schema';
+import { ISchema } from './types';
 
 export type CreateErrorOptions = {
   path?: string;
@@ -21,6 +22,7 @@ export type TestContext<TContext = {}> = {
   path: string;
   options: ValidateOptions<TContext>;
   parent: any;
+  from?: Array<{ schema: ISchema<any, TContext>; value: any }>;
   schema: any; // TODO: Schema<any>;
   resolve: <T>(value: T | Reference<T>) => T;
   createError: (params?: CreateErrorOptions) => ValidationError;
@@ -30,7 +32,7 @@ export type TestFunction<T = unknown, TContext = {}> = (
   this: TestContext<TContext>,
   value: T,
   context: TestContext<TContext>,
-) => boolean | ValidationError | Promise<boolean | ValidationError>;
+) => void | boolean | ValidationError | Promise<boolean | ValidationError>;
 
 export type TestOptions<TSchema extends AnySchema = AnySchema> = {
   value: any;
diff --git a/src/util/isSchema.ts b/src/util/isSchema.ts
index f4c6188..c2e8a68 100644
--- a/src/util/isSchema.ts
+++ b/src/util/isSchema.ts
@@ -1,5 +1,5 @@
-import type { SchemaLike } from '../types';
+import type { ISchema } from './types';
 
-const isSchema = (obj: any): obj is SchemaLike => obj && obj.__isYupSchema__;
+const isSchema = (obj: any): obj is ISchema<any> => obj && obj.__isYupSchema__;
 
-export default isSchema
+export default isSchema;
diff --git a/src/util/objectTypes.ts b/src/util/objectTypes.ts
new file mode 100644
index 0000000..b5686a1
--- /dev/null
+++ b/src/util/objectTypes.ts
@@ -0,0 +1,47 @@
+import type Reference from '../Reference';
+import type { Maybe, Optionals } from '../types';
+import type { ISchema } from './types';
+
+export type ObjectShape = { [k: string]: ISchema<any> | Reference };
+
+export type AnyObject = { [k: string]: any };
+
+export type TypeFromShape<S extends ObjectShape, C> = {
+  [K in keyof S]: S[K] extends ISchema<any, C> ? S[K]['__outputType'] : unknown;
+};
+
+export type DefaultFromShape<Shape extends ObjectShape> = {
+  [K in keyof Shape]: Shape[K] extends ISchema<any>
+    ? Shape[K]['__default']
+    : undefined;
+};
+
+export type MergeObjectTypes<T extends Maybe<AnyObject>, U extends AnyObject> =
+  | ({ [P in keyof T]: P extends keyof U ? U[P] : T[P] } & U)
+  | Optionals<T>;
+
+export type PartialDeep<T> = T extends
+  | string
+  | number
+  | bigint
+  | boolean
+  | null
+  | undefined
+  | symbol
+  | Date
+  ? T | undefined
+  : T extends Array<infer ArrayType>
+  ? Array<PartialDeep<ArrayType>>
+  : T extends ReadonlyArray<infer ArrayType>
+  ? ReadonlyArray<ArrayType>
+  : { [K in keyof T]?: PartialDeep<T[K]> };
+
+type OptionalKeys<T extends {}> = {
+  [k in keyof T]: undefined extends T[k] ? k : never;
+}[keyof T];
+
+type RequiredKeys<T extends object> = Exclude<keyof T, OptionalKeys<T>>;
+
+export type MakePartial<T extends object> = {
+  [k in OptionalKeys<T> as T[k] extends never ? never : k]?: T[k];
+} & { [k in RequiredKeys<T> as T[k] extends never ? never : k]: T[k] };
diff --git a/src/util/types.ts b/src/util/types.ts
index 4a3a09f..31361cb 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -1,77 +1,74 @@
-import type { AnyObject, Preserve } from '../types';
+import type { ResolveOptions } from '../Condition';
+import type { CastOptions, SchemaFieldDescription } from '../schema';
+import type {
+  AnyObject,
+  Callback,
+  Optionals,
+  Preserve,
+  ValidateOptions,
+} from '../types';
 
 export type Defined<T> = T extends undefined ? never : T;
 
 export type NotNull<T> = T extends null ? never : T;
 
-export type TypedSchema = {
-  __type: any;
-  __outputType: any;
-};
-
-export type TypeOf<TSchema extends TypedSchema> = TSchema['__type'];
+export interface ISchema<T, C = AnyObject, F extends Flags = any, D = any> {
+  __flags: F;
+  __context: C;
+  __outputType: T;
+  __default: D;
+
+  cast(value: any, options?: CastOptions<C>): T;
+  validate(
+    value: any,
+    options?: ValidateOptions<C>,
+    maybeCb?: Callback,
+  ): Promise<T>;
+
+  describe(options?: ResolveOptions<C>): SchemaFieldDescription;
+  resolve(options: ResolveOptions<C>): ISchema<T, C, F>;
+}
 
-export type Asserts<TSchema extends TypedSchema> = TSchema['__outputType'];
+export type InferType<T extends ISchema<any, any>> = T['__outputType'];
 
 export type Thunk<T> = T | (() => T);
 
-export type If<T, Y, N> = Exclude<T, undefined> extends never ? Y : N;
-
 /* this seems to force TS to show the full type instead of all the wrapped generics */
 export type _<T> = T extends {} ? { [k in keyof T]: T[k] } : T;
 
-type OptionalKeys<T extends {}> = {
-  [k in keyof T]: undefined extends T[k] ? k : never;
-}[keyof T];
-
-type RequiredKeys<T extends object> = Exclude<keyof T, OptionalKeys<T>>;
-
-export type MakePartial<T extends object> = {
-  [k in OptionalKeys<T>]?: T[k];
-} &
-  { [k in RequiredKeys<T>]: T[k] };
-
 //
 // Schema Config
 //
 
 export type Flags = 's' | 'd' | '';
 
-export interface Config<C = AnyObject, F extends Flags = ''> {
-  context: C;
-  flags: F;
-}
-
-export type AnyConfig = Config<any, any>;
+export type SetFlag<Old extends Flags, F extends Flags> = Exclude<Old, ''> | F;
 
-export type MergeConfig<T extends AnyConfig, U extends AnyConfig> = Config<
-  T['context'] & U['context'],
-  T['flags'] | U['flags']
->;
-
-export type SetFlag<C extends AnyConfig, F extends Flags> = C extends Config<
-  infer Context,
-  infer Old
->
-  ? Config<Context, Exclude<Old, ''> | F>
-  : never;
-
-export type UnsetFlag<C extends AnyConfig, F extends Flags> = C extends Config<
-  infer Context,
-  infer Old
->
-  ? Exclude<Old, F> extends never
-    ? Config<Context, ''>
-    : Config<Context, Exclude<Old, F>>
-  : never;
+export type UnsetFlag<Old extends Flags, F extends Flags> = Exclude<
+  Old,
+  F
+> extends never
+  ? ''
+  : Exclude<Old, F>;
 
-export type ToggleDefault<C extends AnyConfig, D> = Preserve<
+export type ToggleDefault<F extends Flags, D> = Preserve<
   D,
   undefined
 > extends never
-  ? SetFlag<C, 'd'>
-  : UnsetFlag<C, 'd'>;
+  ? SetFlag<F, 'd'>
+  : UnsetFlag<F, 'd'>;
+
+export type ResolveFlags<T, F extends Flags> = Preserve<F, 's'> extends never
+  ? Extract<F, 'd'> extends never
+    ? T
+    : Defined<T>
+  : never;
+
+export type Concat<T, U> = NonNullable<T> & NonNullable<U> extends never
+  ? never
+  : (NonNullable<T> & NonNullable<U>) | Optionals<U>;
 
-// type _s = ToggleDefault<Config<any, 'd'>, undefined>;
+// // $ExpectType string | null
+// type a = Concat<string | null, string>;
 
-// type _f = MergeConfig<Config<AnyObject, 'd'>, Config<AnyObject, ''>>;
+// type b = string  & (string | undefined)
diff --git a/yarn.lock b/yarn.lock
index ed9a9c2..8fe66e2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1342,6 +1342,17 @@
     "@types/yargs" "^16.0.0"
     chalk "^4.0.0"
 
+"@jest/types@^27.4.2":
+  version "27.4.2"
+  resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.4.2.tgz#96536ebd34da6392c2b7c7737d693885b5dd44a5"
+  integrity sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg==
+  dependencies:
+    "@types/istanbul-lib-coverage" "^2.0.0"
+    "@types/istanbul-reports" "^3.0.0"
+    "@types/node" "*"
+    "@types/yargs" "^16.0.0"
+    chalk "^4.0.0"
+
 "@manypkg/find-root@^1.1.0":
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f"
@@ -1612,6 +1623,14 @@
   dependencies:
     "@types/istanbul-lib-report" "*"
 
+"@types/jest@^27.0.3":
+  version "27.0.3"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.3.tgz#0cf9dfe9009e467f70a342f0f94ead19842a783a"
+  integrity sha512-cmmwv9t7gBYt7hNKH5Spu7Kuu/DotGa+Ff+JGRKZ4db5eh8PnKS4LuebJ3YLUoyOyIHraTGyULn23YtEAm0VSg==
+  dependencies:
+    jest-diff "^27.0.0"
+    pretty-format "^27.0.0"
+
 "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.7":
   version "7.0.9"
   resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
@@ -3950,6 +3969,11 @@ diff-sequences@^27.0.6:
   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723"
   integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==
 
+diff-sequences@^27.4.0:
+  version "27.4.0"
+  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5"
+  integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww==
+
 diffie-hellman@^5.0.0:
   version "5.0.3"
   resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@@ -6450,6 +6474,16 @@ jest-diff@^26.0.1:
     jest-get-type "^26.3.0"
     pretty-format "^26.6.1"
 
+jest-diff@^27.0.0:
+  version "27.4.2"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.4.2.tgz#786b2a5211d854f848e2dcc1e324448e9481f36f"
+  integrity sha512-ujc9ToyUZDh9KcqvQDkk/gkbf6zSaeEg9AiBxtttXW59H/AcqEYp1ciXAtJp+jXWva5nAf/ePtSsgWwE5mqp4Q==
+  dependencies:
+    chalk "^4.0.0"
+    diff-sequences "^27.4.0"
+    jest-get-type "^27.4.0"
+    pretty-format "^27.4.2"
+
 jest-diff@^27.2.5:
   version "27.2.5"
   resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.2.5.tgz#908f7a6aca5653824516ad30e0a9fd9767e53623"
@@ -6513,6 +6547,11 @@ jest-get-type@^27.0.6:
   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe"
   integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==
 
+jest-get-type@^27.4.0:
+  version "27.4.0"
+  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.4.0.tgz#7503d2663fffa431638337b3998d39c5e928e9b5"
+  integrity sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ==
+
 jest-haste-map@^27.2.5:
   version "27.2.5"
   resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.2.5.tgz#0247b7299250643472bbcf5b4ad85c72d5178e2e"
@@ -8645,6 +8684,16 @@ pretty-format@^26.6.1:
     ansi-styles "^4.0.0"
     react-is "^17.0.1"
 
+pretty-format@^27.0.0, pretty-format@^27.4.2:
+  version "27.4.2"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.4.2.tgz#e4ce92ad66c3888423d332b40477c87d1dac1fb8"
+  integrity sha512-p0wNtJ9oLuvgOQDEIZ9zQjZffK7KtyR6Si0jnXULIDwrlNF8Cuir3AZP0hHv0jmKuNN/edOnbMjnzd4uTcmWiw==
+  dependencies:
+    "@jest/types" "^27.4.2"
+    ansi-regex "^5.0.1"
+    ansi-styles "^5.0.0"
+    react-is "^17.0.1"
+
 pretty-format@^27.2.5:
   version "27.2.5"
   resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.2.5.tgz#7cfe2a8e8f01a5b5b29296a0b70f4140df0830c5"


Test patch:
diff --git a/test-setup.js b/test-setup.js
index d1452d8..f13dd94 100644
--- a/test-setup.js
+++ b/test-setup.js
@@ -1,22 +1,5 @@
 const { SynchronousPromise } = require('synchronous-promise');
 
-// global.chai = require('chai');
-global.sinon = require('sinon');
-
-// global.chai.use(require('sinon-chai'));
-// global.chai.use(require('chai-as-promised'));
-// global.chai.use(require('dirty-chai'));
-
-// global.expect = global.chai.expect;
-// global.chai.should();
-
-// WTF???
-// Object.defineProperty(
-//   Promise.prototype,
-//   'should',
-//   Object.getOwnPropertyDescriptor(Object.prototype, 'should'),
-// );
-
 global.TestHelpers = require('./test/helpers');
 
 if (global.YUP_USE_SYNC) {
diff --git a/test/ValidationError.js b/test/ValidationError.ts
similarity index 82%
rename from test/ValidationError.js
rename to test/ValidationError.ts
index d60e1a6..6084b50 100644
--- a/test/ValidationError.js
+++ b/test/ValidationError.ts
@@ -1,48 +1,48 @@
 import ValidationError from '../src/ValidationError';
 
-describe('ValidationError', function () {
-  describe('formatError', function () {
-    it('should insert the params into the message', function () {
+describe('ValidationError', () => {
+  describe('formatError', () => {
+    it('should insert the params into the message', () => {
       const str = ValidationError.formatError('Some message ${param}', {
         param: 'here',
       });
       expect(str).toContain('here');
     });
 
-    it(`should auto include any param named 'label' or 'path' as the 'path' param`, function () {
+    it(`should auto include any param named 'label' or 'path' as the 'path' param`, () => {
       const str = ValidationError.formatError('${path} goes here', {
         label: 'label',
       });
       expect(str).toContain('label');
     });
 
-    it(`should use 'this' if a 'label' or 'path' param is not provided`, function () {
+    it(`should use 'this' if a 'label' or 'path' param is not provided`, () => {
       const str = ValidationError.formatError('${path} goes here', {});
       expect(str).toContain('this');
     });
 
-    it(`should include "undefined" in the message if undefined is provided as a param`, function () {
+    it(`should include "undefined" in the message if undefined is provided as a param`, () => {
       const str = ValidationError.formatError('${path} value is ${min}', {
         min: undefined,
       });
       expect(str).toContain('undefined');
     });
 
-    it(`should include "null" in the message if null is provided as a param`, function () {
+    it(`should include "null" in the message if null is provided as a param`, () => {
       const str = ValidationError.formatError('${path} value is ${min}', {
         min: null,
       });
       expect(str).toContain('null');
     });
 
-    it(`should include "NaN" in the message if null is provided as a param`, function () {
+    it(`should include "NaN" in the message if null is provided as a param`, () => {
       const str = ValidationError.formatError('${path} value is ${min}', {
         min: NaN,
       });
       expect(str).toContain('NaN');
     });
 
-    it(`should include 0 in the message if 0 is provided as a param`, function () {
+    it(`should include 0 in the message if 0 is provided as a param`, () => {
       const str = ValidationError.formatError('${path} value is ${min}', {
         min: 0,
       });
diff --git a/test/array.js b/test/array.ts
similarity index 88%
rename from test/array.js
rename to test/array.ts
index 34478d5..a59af99 100644
--- a/test/array.js
+++ b/test/array.ts
@@ -1,4 +1,4 @@
-import { string, number, object, array, StringSchema } from '../src';
+import { string, number, object, array, StringSchema, AnySchema } from '../src';
 
 describe('Array types', () => {
   describe('casting', () => {
@@ -34,7 +34,7 @@ describe('Array types', () => {
   });
 
   it('should type check', () => {
-    var inst = array();
+    let inst = array();
 
     expect(inst.isType([])).toBe(true);
     expect(inst.isType({})).toBe(false);
@@ -56,7 +56,7 @@ describe('Array types', () => {
 
     let merged = array(number()).concat(array(number().required()));
 
-    expect(merged.innerType.type).toBe('number');
+    expect((merged.innerType as AnySchema).type).toBe('number');
 
     await expect(merged.validateAt('[0]', undefined)).rejects.toThrowError();
   });
@@ -75,7 +75,7 @@ describe('Array types', () => {
       ['required', null, array().required()],
       ['null', null, array()],
       ['length', [1, 2, 3], array().length(2)],
-    ])('Basic validations fail: %s %p', async (type, value, schema) => {
+    ])('Basic validations fail: %s %p', async (_, value, schema) => {
       expect(await schema.isValid(value)).toBe(false);
     });
 
@@ -83,12 +83,14 @@ describe('Array types', () => {
       ['required', [], array().required()],
       ['nullable', null, array().nullable()],
       ['length', [1, 2, 3], array().length(3)],
-    ])('Basic validations pass: %s %p', async (type, value, schema) => {
+    ])('Basic validations pass: %s %p', async (_, value, schema) => {
       expect(await schema.isValid(value)).toBe(true);
     });
 
     it('should allow undefined', async () => {
-      await expect(array().of(number().max(5)).isValid()).resolves.toBe(true);
+      await expect(
+        array().of(number().max(5)).isValid(undefined),
+      ).resolves.toBe(true);
     });
 
     it('max should replace earlier tests', async () => {
@@ -100,7 +102,7 @@ describe('Array types', () => {
     });
 
     it('should respect subtype validations', async () => {
-      var inst = array().of(number().max(5));
+      let inst = array().of(number().max(5));
 
       await expect(inst.isValid(['gg', 3])).resolves.toBe(false);
       await expect(inst.isValid([7, 3])).resolves.toBe(false);
@@ -111,9 +113,10 @@ describe('Array types', () => {
     });
 
     it('should prevent recursive casting', async () => {
+      // @ts-ignore
       let castSpy = jest.spyOn(StringSchema.prototype, '_cast');
 
-      let value = await array(string()).validate([5]);
+      let value = await array(string()).defined().validate([5]);
 
       expect(value[0]).toBe('5');
 
@@ -123,7 +126,7 @@ describe('Array types', () => {
   });
 
   it('should respect abortEarly', async () => {
-    var inst = array()
+    let inst = array()
       .of(object({ str: string().required() }))
       .test('name', 'oops', () => false);
 
@@ -145,7 +148,7 @@ describe('Array types', () => {
   });
 
   it('should compact arrays', () => {
-    var arr = ['', 1, 0, 4, false, null],
+    let arr = ['', 1, 0, 4, false, null],
       inst = array();
 
     expect(inst.compact().cast(arr)).toEqual([1, 4]);
@@ -160,7 +163,7 @@ describe('Array types', () => {
   });
 
   it('should ensure arrays', () => {
-    var inst = array().ensure();
+    let inst = array().ensure();
 
     const a = [1, 4];
     expect(inst.cast(a)).toBe(a);
@@ -192,7 +195,8 @@ describe('Array types', () => {
     sparseArray[1] = 1;
     let value = await array().of(number()).validate(sparseArray);
     expect(0 in sparseArray).toBe(false);
-    expect(0 in value).toBe(false);
+    expect(0 in value!).toBe(false);
+
     // eslint-disable-next-line no-sparse-arrays
     expect(value).toEqual([, 1]);
   });
diff --git a/test/bool.js b/test/bool.ts
similarity index 95%
rename from test/bool.js
rename to test/bool.ts
index fba865e..87eafbe 100644
--- a/test/bool.js
+++ b/test/bool.ts
@@ -1,4 +1,5 @@
 import { bool } from '../src';
+import * as TestHelpers from './helpers';
 
 describe('Boolean types', () => {
   it('should CAST correctly', () => {
@@ -47,7 +48,7 @@ describe('Boolean types', () => {
       expect(bool().isValid('1')).resolves.toBe(true),
       expect(bool().strict().isValid(null)).resolves.toBe(false),
       expect(bool().nullable().isValid(null)).resolves.toBe(true),
-      expect(inst.validate()).rejects.toEqual(
+      expect(inst.validate(undefined)).rejects.toEqual(
         expect.objectContaining({
           errors: ['this is a required field'],
         }),
diff --git a/test/date.js b/test/date.ts
similarity index 89%
rename from test/date.js
rename to test/date.ts
index 6fc2ca4..603b949 100644
--- a/test/date.js
+++ b/test/date.ts
@@ -1,12 +1,12 @@
 import { ref, date } from '../src';
 
-function isValidDate(date) {
+function isValidDate(date: any): date is Date {
   return date instanceof Date && !isNaN(date.getTime());
 }
 
 describe('Date types', () => {
   it('should CAST correctly', () => {
-    var inst = date();
+    let inst = date();
 
     expect(inst.cast(new Date())).toBeInstanceOf(Date);
     expect(inst.cast('jan 15 2014')).toEqual(new Date(2014, 0, 15));
@@ -22,14 +22,14 @@ describe('Date types', () => {
   });
 
   it('should return invalid date for failed casts', function () {
-    var inst = date();
+    let inst = date();
 
     expect(isValidDate(inst.cast(null, { assert: false }))).toBe(false);
     expect(isValidDate(inst.cast('', { assert: false }))).toBe(false);
   });
 
   it('should type check', () => {
-    var inst = date();
+    let inst = date();
 
     expect(inst.isType(new Date())).toBe(true);
     expect(inst.isType(false)).toBe(false);
@@ -39,7 +39,7 @@ describe('Date types', () => {
   });
 
   it('should VALIDATE correctly', () => {
-    var inst = date().required().max(new Date(2014, 5, 15));
+    let inst = date().required().max(new Date(2014, 5, 15));
 
     return Promise.all([
       expect(date().isValid(null)).resolves.toBe(false),
@@ -49,7 +49,7 @@ describe('Date types', () => {
       expect(inst.isValid(new Date(2014, 7, 15))).resolves.toBe(false),
       expect(inst.isValid('5')).resolves.toBe(true),
 
-      expect(inst.validate()).rejects.toEqual(
+      expect(inst.validate(undefined)).rejects.toEqual(
         expect.objectContaining({
           errors: ['this is a required field'],
         }),
@@ -58,7 +58,7 @@ describe('Date types', () => {
   });
 
   it('should check MIN correctly', () => {
-    var min = new Date(2014, 3, 15),
+    let min = new Date(2014, 3, 15),
       invalid = new Date(2014, 1, 15),
       valid = new Date(2014, 5, 15);
     expect(function () {
@@ -87,7 +87,7 @@ describe('Date types', () => {
   });
 
   it('should check MAX correctly', () => {
-    var max = new Date(2014, 7, 15),
+    let max = new Date(2014, 7, 15),
       invalid = new Date(2014, 9, 15),
       valid = new Date(2014, 5, 15);
     expect(function () {
@@ -100,7 +100,7 @@ describe('Date types', () => {
     return Promise.all([
       expect(date().max(max).isValid(valid)).resolves.toBe(true),
       expect(date().max(max).isValid(invalid)).resolves.toBe(false),
-      expect(date().max(max).nullable(true).isValid(null)).resolves.toBe(true),
+      expect(date().max(max).nullable().isValid(null)).resolves.toBe(true),
 
       expect(
         date()
diff --git a/test/helpers.js b/test/helpers.ts
similarity index 63%
rename from test/helpers.js
rename to test/helpers.ts
index 0c18700..42292d8 100644
--- a/test/helpers.js
+++ b/test/helpers.ts
@@ -1,10 +1,18 @@
+import { ISchema } from '../src/types';
 import printValue from '../src/util/printValue';
 
-export let castAndShouldFail = (schema, value) => {
+export let castAndShouldFail = (schema: ISchema<any>, value: any) => {
   expect(() => schema.cast(value)).toThrowError(TypeError);
 };
 
-export let castAll = (inst, { invalid = [], valid = [] }) => {
+type Options = {
+  invalid?: any[];
+  valid?: any[];
+};
+export let castAll = (
+  inst: ISchema<any>,
+  { invalid = [], valid = [] }: Options,
+) => {
   valid.forEach(([value, result, schema = inst]) => {
     it(`should cast ${printValue(value)} to ${printValue(result)}`, () => {
       expect(schema.cast(value)).toBe(result);
@@ -18,7 +26,10 @@ export let castAll = (inst, { invalid = [], valid = [] }) => {
   });
 };
 
-export let validateAll = (inst, { valid = [], invalid = [] }) => {
+export let validateAll = (
+  inst: ISchema<any>,
+  { valid = [], invalid = [] }: Options,
+) => {
   describe('valid:', () => {
     runValidations(valid, true);
   });
@@ -27,7 +38,7 @@ export let validateAll = (inst, { valid = [], invalid = [] }) => {
     runValidations(invalid, false);
   });
 
-  function runValidations(arr, isValid) {
+  function runValidations(arr: any[], isValid: boolean) {
     arr.forEach((config) => {
       let message = '',
         value = config,
@@ -36,25 +47,25 @@ export let validateAll = (inst, { valid = [], invalid = [] }) => {
       if (Array.isArray(config)) [value, schema, message = ''] = config;
 
       it(`${printValue(value)}${message && `  (${message})`}`, async () => {
-        await expect(schema.isValid(value)).resolves.toEqual(isValid);
+        await expect((schema as any).isValid(value)).resolves.toEqual(isValid);
       });
     });
   }
 };
 
-export function validationErrorWithMessages(...errors) {
+export function validationErrorWithMessages(...errors: any[]) {
   return expect.objectContaining({
     errors,
   });
 }
 
-export function ensureSync(fn) {
+export function ensureSync(fn: () => Promise<any>) {
   let run = false;
-  let resolve = (t) => {
+  let resolve = (t: any) => {
     if (!run) return t;
     throw new Error('Did not execute synchronously');
   };
-  let err = (t) => {
+  let err = (t: any) => {
     if (!run) throw t;
     throw new Error('Did not execute synchronously');
   };
diff --git a/test/lazy.js b/test/lazy.ts
similarity index 66%
rename from test/lazy.js
rename to test/lazy.ts
index e78bae6..b98bb53 100644
--- a/test/lazy.js
+++ b/test/lazy.ts
@@ -1,13 +1,14 @@
-import { lazy, mixed } from '../src';
+import { lazy, mixed, AnyObject, MixedSchemaClass } from '../src';
 
 describe('lazy', function () {
   it('should throw on a non-schema value', () => {
-    expect(() => lazy(() => undefined).validate()).toThrowError();
+    // @ts-expect-error testing incorrect usage
+    expect(() => lazy(() => undefined).validate(undefined)).toThrowError();
   });
 
   describe('mapper', () => {
     const value = 1;
-    let mapper;
+    let mapper: jest.Mock<MixedSchemaClass<any, AnyObject, undefined, ''>, []>;
 
     beforeEach(() => {
       mapper = jest.fn(() => mixed());
@@ -22,8 +23,9 @@ describe('lazy', function () {
       const context = {
         a: 1,
       };
-      lazy(mapper).validate(value, context);
-      expect(mapper).toHaveBeenCalledWith(value, context);
+      let options = { context };
+      lazy(mapper).validate(value, options);
+      expect(mapper).toHaveBeenCalledWith(value, options);
     });
 
     it('should allow meta', () => {
diff --git a/test/mixed.js b/test/mixed.ts
similarity index 90%
rename from test/mixed.js
rename to test/mixed.ts
index b156c8c..167dea4 100644
--- a/test/mixed.js
+++ b/test/mixed.ts
@@ -9,19 +9,20 @@ import {
   ref,
   string,
   ValidationError,
-  MixedSchema,
 } from '../src';
-import { ensureSync } from './helpers';
+import ObjectSchema from '../src/object';
+import { ISchema } from '../src/util/types';
+import { ensureSync, validateAll } from './helpers';
 
 let noop = () => {};
 
+// @ts-ignore
 global.YUP_USE_SYNC &&
   it('[internal] normal methods should be running in sync Mode', async () => {
     let schema = number();
 
     // test negative ensure case
     await expect(ensureSync(() => Promise.resolve())).rejects.toThrowError(
-      Error,
       'Did not execute synchronously',
     );
 
@@ -37,25 +38,6 @@ global.YUP_USE_SYNC &&
   });
 
 describe('Mixed Types ', () => {
-  xit('should be immutable', () => {
-    let inst = mixed(),
-      next;
-    let sub = (inst.sub = mixed());
-
-    expect(inst).not.toBe((next = inst.required()));
-
-    expect(next.sub).toBe(sub);
-    expect(inst.sub).toBe(next.sub);
-
-    expect(inst).toBeInstanceOf(MixedSchema);
-    expect(next).toBeInstanceOf(MixedSchema);
-
-    return Promise.all([
-      expect(inst.isValid()).resolves.toBe(true),
-      next.isValid(null),
-    ]);
-  });
-
   it('cast should return a default when undefined', () => {
     let inst = mixed().default('hello');
 
@@ -71,7 +53,7 @@ describe('Mixed Types ', () => {
   it('getDefault should return the default value using context', function () {
     let inst = string().when('$foo', {
       is: 'greet',
-      then: string().default('hi'),
+      then: (s) => s.default('hi'),
     });
     expect(inst.getDefault({ context: { foo: 'greet' } })).toBe('hi');
   });
@@ -134,7 +116,7 @@ describe('Mixed Types ', () => {
       string()
         .label('My string')
         .required((d) => `${d.label} is required`)
-        .validate(),
+        .validate(undefined),
     ).rejects.toThrowError(/My string is required/);
   });
 
@@ -193,15 +175,16 @@ describe('Mixed Types ', () => {
     await expect(inst.isValid('test')).resolves.toBe(true);
     await expect(inst.isValid(1)).resolves.toBe(true);
 
-    await expect(inst.validate()).rejects.toThrowError(
+    await expect(inst.validate(undefined)).rejects.toThrowError(
       'this is a required field',
     );
 
     inst = inst.notRequired();
 
-    await expect(inst.isValid()).resolves.toBe(true);
+    await expect(inst.isValid(undefined)).resolves.toBe(true);
   });
 
+  // @ts-ignore
   global.YUP_USE_SYNC &&
     describe('synchronous methods', () => {
       it('should validate synchronously', async () => {
@@ -221,7 +204,7 @@ describe('Mixed Types ', () => {
       });
 
       it('should throw on async test', async () => {
-        let schema = mixed().test('test', 'foo', () => Promise.resolve());
+        let schema = mixed().test('test', 'foo', () => Promise.resolve(true));
 
         await expect(
           ensureSync(() => schema.validate('john')),
@@ -232,7 +215,7 @@ describe('Mixed Types ', () => {
   describe('oneOf', () => {
     let inst = mixed().oneOf(['hello']);
 
-    TestHelpers.validateAll(inst, {
+    validateAll(inst, {
       valid: [undefined, 'hello'],
       invalid: [
         'YOLO',
@@ -261,7 +244,7 @@ describe('Mixed Types ', () => {
   describe('should exclude values', () => {
     let inst = mixed().nullable().notOneOf([5, 'hello']);
 
-    TestHelpers.validateAll(inst, {
+    validateAll(inst, {
       valid: [6, 'hfhfh', [5, inst.oneOf([5]), '`oneOf` called after'], null],
       invalid: [5, [null, inst.required(), 'required schema']],
     });
@@ -318,10 +301,7 @@ describe('Mixed Types ', () => {
   it('should fallback to default message', async () => {
     let inst = mixed().test(() => false);
 
-    await expect(inst.validate('foo')).rejects.toThrowError(
-      ValidationError,
-      'this is invalid',
-    );
+    await expect(inst.validate('foo')).rejects.toThrowError('this is invalid');
   });
 
   it('should allow non string messages', async () => {
@@ -349,14 +329,14 @@ describe('Mixed Types ', () => {
   });
 
   it('should respect exclusive validation', () => {
-    let inst = mixed()
-      .test({
-        message: 'invalid',
-        exclusive: true,
-        name: 'test',
-        test: () => {},
-      })
-      .test({ message: 'also invalid', name: 'test', test: () => {} });
+    let inst = mixed().test({
+      message: 'invalid',
+      exclusive: true,
+      name: 'test',
+      test: () => {},
+    });
+
+    //.test({ message: 'also invalid', name: 'test', test: () => {} });
 
     expect(inst.tests).toHaveLength(1);
 
@@ -447,7 +427,7 @@ describe('Mixed Types ', () => {
   });
 
   it('tests should be able to access nested parent', async () => {
-    let finalFrom, finalOptions;
+    let finalFrom: any, finalOptions: any;
     let testFixture = {
       firstField: 'test',
       second: [
@@ -463,7 +443,7 @@ describe('Mixed Types ', () => {
     let third = object({
       thirdField: mixed().test({
         test() {
-          finalFrom = this.from;
+          finalFrom = this.from!;
           finalOptions = this.options;
           return true;
         },
@@ -525,6 +505,7 @@ describe('Mixed Types ', () => {
     return expect(inst.validate('joe')).rejects.toThrowError('test a');
   });
 
+  // @ts-ignore
   !global.YUP_USE_SYNC &&
     it('should fail when the test function returns a rejected Promise', async () => {
       let inst = string().test(() => {
@@ -586,7 +567,7 @@ describe('Mixed Types ', () => {
   });
 
   describe('concat', () => {
-    let next;
+    let next: ISchema<any>;
     let inst = object({
       str: string().required(),
       obj: object({
@@ -653,6 +634,7 @@ describe('Mixed Types ', () => {
     let inst = string().default('hi');
 
     expect(function () {
+      // @ts-expect-error invalid combo
       inst.concat(object());
     }).toThrowError(TypeError);
   });
@@ -682,8 +664,7 @@ describe('Mixed Types ', () => {
     let inst = mixed().concat(number());
 
     await expect(inst.validate([])).rejects.toThrowError(
-      ValidationError,
-      /should be a `number`/,
+      /must be a `number` type/,
     );
   });
 
@@ -701,12 +682,6 @@ describe('Mixed Types ', () => {
     await expect(inst.isValid('a')).resolves.toBe(true);
   });
 
-  // xit('concat should maintain explicit nullability', async function () {
-  //   let inst = string().nullable().concat(string().default('hi'));
-
-  //   await inst.isValid(null).should.resolves.toBe(true);
-  // });
-
   it('concat should maintain explicit presence', async function () {
     let inst = string().required().concat(string());
 
@@ -757,44 +732,44 @@ describe('Mixed Types ', () => {
   it('should handle conditionals', async function () {
     let inst = mixed().when('prop', {
       is: 5,
-      then: mixed().required('from parent'),
+      then: (s) => s.required('from parent'),
     });
 
     await expect(
-      inst.validate(undefined, { parent: { prop: 5 } }),
+      inst.validate(undefined, { parent: { prop: 5 } } as any),
     ).rejects.toThrowError();
     await expect(
-      inst.validate(undefined, { parent: { prop: 1 } }),
+      inst.validate(undefined, { parent: { prop: 1 } } as any),
     ).resolves.toBeUndefined();
     await expect(
-      inst.validate('hello', { parent: { prop: 5 } }),
+      inst.validate('hello', { parent: { prop: 5 } } as any),
     ).resolves.toBeDefined();
 
     inst = string().when('prop', {
-      is: function (val) {
-        return val === 5;
-      },
-      then: string().required(),
-      otherwise: string().min(4),
+      is: 5,
+      then: (s) => s.required(),
+      otherwise: (s) => s.min(4),
     });
 
     await expect(
-      inst.validate(undefined, { parent: { prop: 5 } }),
+      inst.validate(undefined, { parent: { prop: 5 } } as any),
     ).rejects.toThrowError();
     await expect(
-      inst.validate('hello', { parent: { prop: 1 } }),
+      inst.validate('hello', { parent: { prop: 1 } } as any),
     ).resolves.toBeDefined();
     await expect(
-      inst.validate('hel', { parent: { prop: 1 } }),
+      inst.validate('hel', { parent: { prop: 1 } } as any),
     ).rejects.toThrowError();
   });
 
   it('should handle multiple conditionals', function () {
     let called = false;
-    let inst = mixed().when(['$prop', '$other'], function (prop, other) {
+    let inst = mixed().when(['$prop', '$other'], (prop, other) => {
       expect(other).toBe(true);
       expect(prop).toBe(1);
       called = true;
+
+      return mixed();
     });
 
     inst.cast({}, { context: { prop: 1, other: true } });
@@ -802,7 +777,7 @@ describe('Mixed Types ', () => {
 
     inst = mixed().when(['$prop', '$other'], {
       is: 5,
-      then: mixed().required(),
+      then: (s) => s.required(),
     });
 
     return expect(
@@ -813,7 +788,7 @@ describe('Mixed Types ', () => {
   it('should require context when needed', async function () {
     let inst = mixed().when('$prop', {
       is: 5,
-      then: mixed().required('from context'),
+      then: (s) => s.required('from context'),
     });
 
     await expect(
@@ -827,11 +802,9 @@ describe('Mixed Types ', () => {
     ).resolves.toBeDefined();
 
     inst = string().when('$prop', {
-      is: function (val) {
-        return val === 5;
-      },
-      then: string().required(),
-      otherwise: string().min(4),
+      is: (val: any) => val === 5,
+      then: (s) => s.required(),
+      otherwise: (s) => s.min(4),
     });
 
     await expect(
@@ -849,7 +822,7 @@ describe('Mixed Types ', () => {
     let inst = object({
       prop: string().when('$prop', {
         is: 5,
-        then: string().required('from context'),
+        then: (s) => s.required('from context'),
       }),
     });
 
@@ -858,14 +831,11 @@ describe('Mixed Types ', () => {
 
   it('should support self references in conditions', async function () {
     let inst = number().when('.', {
-      is: (value) => value > 0,
-      then: number().min(5),
+      is: (value: number) => value > 0,
+      then: (s) => s.min(5),
     });
 
-    await expect(inst.validate(4)).rejects.toThrowError(
-      ValidationError,
-      /must be greater/,
-    );
+    await expect(inst.validate(4)).rejects.toThrowError(/must be greater/);
 
     await expect(inst.validate(5)).resolves.toBeDefined();
 
@@ -874,14 +844,11 @@ describe('Mixed Types ', () => {
 
   it('should support conditional single argument as options shortcut', async function () {
     let inst = number().when({
-      is: (value) => value > 0,
-      then: number().min(5),
+      is: (value: number) => value > 0,
+      then: (s) => s.min(5),
     });
 
-    await expect(inst.validate(4)).rejects.toThrowError(
-      ValidationError,
-      /must be greater/,
-    );
+    await expect(inst.validate(4)).rejects.toThrowError(/must be greater/);
 
     await expect(inst.validate(5)).resolves.toBeDefined();
 
@@ -890,11 +857,12 @@ describe('Mixed Types ', () => {
 
   it('should allow nested conditions and lazies', async function () {
     let inst = string().when('$check', {
-      is: (value) => typeof value === 'string',
-      then: string().when('$check', {
-        is: (value) => /hello/.test(value),
-        then: lazy(() => string().min(6)),
-      }),
+      is: (value: any) => typeof value === 'string',
+      then: (s) =>
+        s.when('$check', {
+          is: (value: any) => /hello/.test(value),
+          then: () => lazy(() => string().min(6)),
+        }),
     });
 
     await expect(
@@ -903,7 +871,7 @@ describe('Mixed Types ', () => {
 
     await expect(
       inst.validate('pass', { context: { check: 'hello' } }),
-    ).rejects.toThrowError(ValidationError, /must be at least/);
+    ).rejects.toThrowError(/must be at least/);
 
     await expect(
       inst.validate('passes', { context: { check: 'hello' } }),
@@ -931,7 +899,7 @@ describe('Mixed Types ', () => {
   });
 
   describe('schema.describe()', () => {
-    let schema;
+    let schema: ObjectSchema<any>;
     beforeEach(() => {
       schema = object({
         lazy: lazy(() => string().nullable()),
diff --git a/test/number.js b/test/number.ts
similarity index 89%
rename from test/number.js
rename to test/number.ts
index 351d48c..edfdd02 100644
--- a/test/number.js
+++ b/test/number.ts
@@ -3,11 +3,6 @@ import * as TestHelpers from './helpers';
 import { number, NumberSchema } from '../src';
 
 describe('Number types', function () {
-  // it('is newable', () => {
-  //   let schema = new number();
-  //   schema.integer().required();
-  // });
-
   it('is extensible', () => {
     class MyNumber extends NumberSchema {
       foo() {
@@ -32,12 +27,14 @@ describe('Number types', function () {
     });
 
     it('should round', () => {
-      // schema.round('floor').cast(45.99999).should.equal(45);
+      // @ts-expect-error stricter type than accepted
       expect(schema.round('ceIl').cast(45.1111)).toBe(46);
+
       expect(schema.round().cast(45.444444)).toBe(45);
 
       expect(schema.nullable().integer().round().cast(null)).toBeNull();
       expect(function () {
+        // @ts-expect-error testing incorrectness
         schema.round('fasf');
       }).toThrowError(TypeError);
     });
@@ -54,14 +51,14 @@ describe('Number types', function () {
   });
 
   it('should handle DEFAULT', function () {
-    var inst = number().default(0);
+    let inst = number().default(0);
 
     expect(inst.getDefault()).toBe(0);
     expect(inst.default(5).required().getDefault()).toBe(5);
   });
 
   it('should type check', function () {
-    var inst = number();
+    let inst = number();
 
     expect(inst.isType(5)).toBe(true);
     expect(inst.isType(new Number(5))).toBe(true);
@@ -73,7 +70,7 @@ describe('Number types', function () {
   });
 
   it('should VALIDATE correctly', function () {
-    var inst = number().required().min(4);
+    let inst = number().required().min(4);
 
     return Promise.all([
       expect(number().isValid(null)).resolves.toBe(false),
@@ -86,7 +83,7 @@ describe('Number types', function () {
       expect(inst.isValid(5)).resolves.toBe(true),
       expect(inst.isValid(2)).resolves.toBe(false),
 
-      expect(inst.validate()).rejects.toEqual(
+      expect(inst.validate(undefined)).rejects.toEqual(
         TestHelpers.validationErrorWithMessages(
           expect.stringContaining('required'),
         ),
@@ -95,7 +92,7 @@ describe('Number types', function () {
   });
 
   describe('min', () => {
-    var schema = number().min(5);
+    let schema = number().min(5);
 
     TestHelpers.validateAll(schema, {
       valid: [7, 35738787838, [null, schema.nullable()]],
@@ -104,7 +101,7 @@ describe('Number types', function () {
   });
 
   describe('max', () => {
-    var schema = number().max(5);
+    let schema = number().max(5);
 
     TestHelpers.validateAll(schema, {
       valid: [4, -5222, [null, schema.nullable()]],
@@ -113,7 +110,7 @@ describe('Number types', function () {
   });
 
   describe('lessThan', () => {
-    var schema = number().lessThan(5);
+    let schema = number().lessThan(5);
 
     TestHelpers.validateAll(schema, {
       valid: [4, -10, [null, schema.nullable()]],
@@ -128,7 +125,7 @@ describe('Number types', function () {
   });
 
   describe('moreThan', () => {
-    var schema = number().moreThan(5);
+    let schema = number().moreThan(5);
 
     TestHelpers.validateAll(schema, {
       valid: [6, 56445435, [null, schema.nullable()]],
@@ -145,7 +142,7 @@ describe('Number types', function () {
   });
 
   describe('integer', () => {
-    var schema = number().integer();
+    let schema = number().integer();
 
     TestHelpers.validateAll(schema, {
       valid: [4, -5222, 3.12312e51],
@@ -160,7 +157,7 @@ describe('Number types', function () {
   });
 
   it('should check POSITIVE correctly', function () {
-    var v = number().positive();
+    let v = number().positive();
 
     return Promise.all([
       expect(v.isValid(7)).resolves.toBe(true),
@@ -176,7 +173,7 @@ describe('Number types', function () {
   });
 
   it('should check NEGATIVE correctly', function () {
-    var v = number().negative();
+    let v = number().negative();
 
     return Promise.all([
       expect(v.isValid(-4)).resolves.toBe(true),
diff --git a/test/object.js b/test/object.ts
similarity index 89%
rename from test/object.js
rename to test/object.ts
index 2a7a997..2afb9b1 100644
--- a/test/object.js
+++ b/test/object.ts
@@ -12,14 +12,14 @@ import {
   StringSchema,
   MixedSchema,
 } from '../src';
+import ObjectSchema from '../src/object';
+import { AnyObject } from '../src/types';
 import { validationErrorWithMessages } from './helpers';
 
 describe('Object types', () => {
   describe('casting', () => {
-    let inst;
-
-    beforeEach(() => {
-      inst = object({
+    let createInst = () =>
+      object({
         num: number(),
         str: string(),
         arr: array().of(number()),
@@ -28,6 +28,11 @@ describe('Object types', () => {
         arrNested: array().of(object().shape({ num: number() })),
         stripped: string().strip(),
       });
+
+    let inst: ReturnType<typeof createInst>;
+
+    beforeEach(() => {
+      inst = createInst();
     });
 
     it('should parse json strings', () => {
@@ -61,7 +66,7 @@ describe('Object types', () => {
         arrNested: [{ num: 5 }, { num: 5 }],
       });
 
-      expect(cast.arrNested[0]).toBe(obj.arrNested[0]);
+      expect(cast.arrNested![0]).toBe(obj.arrNested[0]);
     });
 
     it('should return the same object if all props are already cast', () => {
@@ -79,20 +84,18 @@ describe('Object types', () => {
   });
 
   describe('validation', () => {
-    let inst, obj;
-
-    beforeEach(() => {
-      inst = object().shape({
-        num: number().max(4),
+    it('should run validations recursively', async () => {
+      let inst = object({
+        num: number(),
         str: string(),
-        arr: array().of(number().max(6)),
+        arr: array().of(number()),
         dte: date(),
-        nested: object()
-          .shape({ str: string().min(3) })
-          .required(),
+        nested: object().shape({ str: string().strict() }),
         arrNested: array().of(object().shape({ num: number() })),
+        stripped: string().strip(),
       });
-      obj = {
+
+      let obj: AnyObject = {
         num: '4',
         str: 'hello',
         arr: ['4', 5, 6],
@@ -100,27 +103,25 @@ describe('Object types', () => {
         nested: { str: 5 },
         arrNested: [{ num: 5 }, { num: '2' }],
       };
-    });
 
-    it('should run validations recursively', async () => {
-      await expect(inst.isValid()).resolves.toBe(true);
+      await expect(inst.isValid(undefined)).resolves.toBe(true);
 
       await expect(inst.validate(obj)).rejects.toEqual(
         validationErrorWithMessages(expect.stringContaining('nested.str')),
       );
 
       obj.nested.str = 'hello';
-      obj.arr[1] = 8;
+      obj.arrNested[1] = 8;
 
       await expect(inst.validate(obj)).rejects.toEqual(
-        validationErrorWithMessages(expect.stringContaining('arr[1]')),
+        validationErrorWithMessages(expect.stringContaining('arrNested[1]')),
       );
     });
 
     it('should prevent recursive casting', async () => {
-      let castSpy = jest.spyOn(StringSchema.prototype, '_cast');
+      let castSpy = jest.spyOn(StringSchema.prototype, '_cast' as any);
 
-      inst = object({
+      let inst = object({
         field: string(),
       });
 
@@ -134,7 +135,7 @@ describe('Object types', () => {
     });
 
     it('should respect strict for nested values', async () => {
-      inst = object({
+      let inst = object({
         field: string(),
       }).strict();
 
@@ -144,7 +145,7 @@ describe('Object types', () => {
     });
 
     it('should respect strict for nested object values', async () => {
-      inst = object({
+      let inst = object({
         obj: object({
           field: string().strict(),
         }),
@@ -156,7 +157,7 @@ describe('Object types', () => {
     });
 
     it('should respect child schema with strict()', async () => {
-      inst = object({
+      let inst = object({
         field: number().strict(),
       });
 
@@ -185,9 +186,20 @@ describe('Object types', () => {
     });
 
     it('should not clone during validating', async function () {
+      let inst = object({
+        num: number(),
+        str: string(),
+        arr: array().of(number()),
+        dte: date(),
+        nested: object().shape({ str: string() }),
+        arrNested: array().of(object().shape({ num: number() })),
+        stripped: string().strip(),
+      });
+
       let base = MixedSchema.prototype.clone;
 
       MixedSchema.prototype.clone = function (...args) {
+        // @ts-expect-error private property
         if (!this._mutate) throw new Error('should not call clone');
 
         return base.apply(this, args);
@@ -240,14 +252,17 @@ describe('Object types', () => {
   });
 
   describe('object defaults', () => {
-    let objSchema;
-
-    beforeEach(() => {
-      objSchema = object({
+    const createSchema = () =>
+      object({
         nest: object({
           str: string().default('hi'),
         }),
       });
+
+    let objSchema: ReturnType<typeof createSchema>;
+
+    beforeEach(() => {
+      objSchema = createSchema();
     });
 
     it('should expand objects by default', () => {
@@ -257,9 +272,9 @@ describe('Object types', () => {
     });
 
     it('should accept a user provided default', () => {
-      objSchema = objSchema.default({ boom: 'hi' });
+      let schema = objSchema.default({ boom: 'hi' });
 
-      expect(objSchema.getDefault()).toEqual({
+      expect(schema.getDefault()).toEqual({
         boom: 'hi',
       });
     });
@@ -424,13 +439,19 @@ describe('Object types', () => {
 
       try {
         await inst.validate('  john  ');
-      } catch (err) {
+      } catch (err: any) {
         expect(err.message).toBe('trim me!');
       }
     });
 
     it('should resolve to schema', () => {
-      let inst = object({
+      type Nested = {
+        nested: Nested;
+        x: {
+          y: Nested;
+        };
+      };
+      let inst: ObjectSchema<Nested> = object({
         nested: lazy(() => inst),
         x: object({
           y: lazy(() => inst),
@@ -465,13 +486,19 @@ describe('Object types', () => {
     });
 
     it('should always return a schema', () => {
+      // @ts-expect-error Incorrect usage
       expect(() => lazy(() => {}).cast()).toThrowError(
         /must return a valid schema/,
       );
     });
 
     it('should set the correct path', async () => {
-      let inst = object({
+      type Nested = {
+        str: string | null;
+        nested: Nested;
+      };
+
+      let inst: ObjectSchema<Nested> = object({
         str: string().required().nullable(),
         nested: lazy(() => inst.default(undefined)),
       });
@@ -483,14 +510,14 @@ describe('Object types', () => {
 
       try {
         await inst.validate(value, { strict: true });
-      } catch (err) {
+      } catch (err: any) {
         expect(err.path).toBe('nested.str');
         expect(err.message).toMatch(/required/);
       }
     });
 
     it('should set the correct path with dotted keys', async () => {
-      let inst = object({
+      let inst: ObjectSchema<any> = object({
         'dotted.str': string().required().nullable(),
         nested: lazy(() => inst.default(undefined)),
       });
@@ -502,14 +529,14 @@ describe('Object types', () => {
 
       try {
         await inst.validate(value, { strict: true });
-      } catch (err) {
+      } catch (err: any) {
         expect(err.path).toBe('nested["dotted.str"]');
         expect(err.message).toMatch(/required/);
       }
     });
 
     it('should resolve array sub types', async () => {
-      let inst = object({
+      let inst: ObjectSchema<any> = object({
         str: string().required().nullable(),
         nested: array().of(lazy(() => inst.default(undefined))),
       });
@@ -521,14 +548,16 @@ describe('Object types', () => {
 
       try {
         await inst.validate(value, { strict: true });
-      } catch (err) {
+      } catch (err: any) {
         expect(err.path).toBe('nested[0].str');
         expect(err.message).toMatch(/required/);
       }
     });
 
     it('should resolve for each array item', async () => {
-      let inst = array().of(lazy((value) => types[typeof value]));
+      let inst = array().of(
+        lazy((value: string | number) => (types as any)[typeof value]),
+      );
 
       let val = await inst.validate(['john', 4], { strict: true });
 
@@ -621,6 +650,7 @@ describe('Object types', () => {
       foo: object({
         bar: string(),
       }),
+      // @ts-expect-error FIXME
     }).from('foo.bar', 'foobar', true);
 
     expect(inst.cast({ foo: { bar: 'quz' } })).toEqual({
@@ -644,12 +674,14 @@ describe('Object types', () => {
   it('should handle conditionals', () => {
     let inst = object().shape({
       noteDate: number()
-        .when('stats.isBig', { is: true, then: number().min(5) })
+        .when('stats.isBig', { is: true, then: (s) => s.min(5) })
         .when('other', function (v) {
           if (v === 4) return this.max(6);
         }),
       stats: object({ isBig: bool() }),
-      other: number().min(1).when('stats', { is: 5, then: number() }),
+      other: number()
+        .min(1)
+        .when('stats', { is: 5, then: (s) => s }),
     });
 
     return Promise.all([
@@ -689,7 +721,7 @@ describe('Object types', () => {
     let inst = object().shape({
       value: number().when('isRequired', {
         is: true,
-        then: number().required(),
+        then: (s) => s.required(),
       }),
     });
 
@@ -725,7 +757,7 @@ describe('Object types', () => {
       knownDependency: bool(),
       value: number().when(['unknownDependency', 'knownDependency'], {
         is: true,
-        then: number().required(),
+        then: (s) => s.required(),
       }),
     });
 
@@ -748,11 +780,11 @@ describe('Object types', () => {
   it('should allow opt out of topo sort on specific edges', () => {
     expect(() => {
       object().shape({
-        orgID: number().when('location', function (v) {
-          if (v == null) return this.required();
+        orgID: number().when('location', (v, schema) => {
+          if (v == null) return schema.required();
         }),
-        location: string().when('orgID', function (v) {
-          if (v == null) return this.required();
+        location: string().when('orgID', (v, schema) => {
+          if (v == null) return schema.required();
         }),
       });
     }).toThrowError('Cyclic dependency, node was:"location"');
@@ -787,11 +819,11 @@ describe('Object types', () => {
       {
         a1: string().when('a2', {
           is: undefined,
-          then: string().required(),
+          then: (s) => s.required(),
         }),
         a2: string().when('a1', {
           is: undefined,
-          then: string().required(),
+          then: (s) => s.required(),
         }),
       },
       [['a1', 'a2']],
@@ -805,7 +837,7 @@ describe('Object types', () => {
   it('should handle nested conditionals', () => {
     let countSchema = number().when('isBig', {
       is: true,
-      then: number().min(5),
+      then: (s) => s.min(5),
     });
     let inst = object({
       other: bool(),
@@ -814,7 +846,7 @@ describe('Object types', () => {
         count: countSchema,
       })
         .default(undefined)
-        .when('other', { is: true, then: object().required() }),
+        .when('other', { is: true, then: (s) => s.required() }),
     });
 
     return Promise.all([
@@ -912,14 +944,4 @@ describe('Object types', () => {
       await inst.omit(['age', 'name']).validate({ color: 'mauve' }),
     ).toEqual({ color: 'mauve' });
   });
-
-  xit('should handle invalid shapes better', async () => {
-    var schema = object().shape({
-      permissions: undefined,
-    });
-
-    expect(
-      await schema.isValid({ permissions: [] }, { abortEarly: false }),
-    ).toBe(true);
-  });
 });
diff --git a/test/setLocale.js b/test/setLocale.ts
similarity index 82%
rename from test/setLocale.js
rename to test/setLocale.ts
index afe3534..9b59768 100644
--- a/test/setLocale.js
+++ b/test/setLocale.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-var-requires */
 import { setLocale } from '../src';
 
 describe('Custom locale', () => {
@@ -25,7 +26,9 @@ describe('Custom locale', () => {
   });
 
   it('should not allow prototype pollution', () => {
-    const payload = JSON.parse('{"__proto__":{"polluted":"Yes! Its Polluted"}}');
+    const payload = JSON.parse(
+      '{"__proto__":{"polluted":"Yes! Its Polluted"}}',
+    );
 
     expect(() => setLocale(payload)).toThrowError();
 
@@ -33,7 +36,7 @@ describe('Custom locale', () => {
   });
 
   it('should not pollute Object.prototype builtins', () => {
-    const payload = { toString: { polluted: 'oh no' } };
+    const payload: any = { toString: { polluted: 'oh no' } };
 
     expect(() => setLocale(payload)).toThrowError();
 
diff --git a/test/string.js b/test/string.ts
similarity index 93%
rename from test/string.js
rename to test/string.ts
index cd45ba8..2e7f322 100644
--- a/test/string.js
+++ b/test/string.ts
@@ -61,13 +61,13 @@ describe('String types', () => {
   });
 
   it('should handle DEFAULT', function () {
-    var inst = string();
+    let inst = string();
 
     expect(inst.default('my_value').required().getDefault()).toBe('my_value');
   });
 
   it('should type check', function () {
-    var inst = string();
+    let inst = string();
 
     expect(inst.isType('5')).toBe(true);
     expect(inst.isType(new String('5'))).toBe(true);
@@ -77,14 +77,12 @@ describe('String types', () => {
   });
 
   it('should VALIDATE correctly', function () {
-    var inst = string().required().min(4).strict();
+    let inst = string().required().min(4).strict();
 
     return Promise.all([
       expect(string().strict().isValid(null)).resolves.toBe(false),
 
-      expect(string().strict().nullable(true).isValid(null)).resolves.toBe(
-        true,
-      ),
+      expect(string().strict().nullable().isValid(null)).resolves.toBe(true),
 
       expect(inst.isValid('hello')).resolves.toBe(true),
 
@@ -97,7 +95,7 @@ describe('String types', () => {
   });
 
   it('should check MATCHES correctly', function () {
-    var v = string().matches(/(hi|bye)/, 'A message');
+    let v = string().matches(/(hi|bye)/, 'A message');
 
     return Promise.all([
       expect(v.isValid('hi')).resolves.toBe(true),
@@ -107,7 +105,7 @@ describe('String types', () => {
   });
 
   it('should check MATCHES correctly with global and sticky flags', function () {
-    var v = string().matches(/hi/gy);
+    let v = string().matches(/hi/gy);
 
     return Promise.all([
       expect(v.isValid('hi')).resolves.toBe(true),
@@ -134,8 +132,8 @@ describe('String types', () => {
   });
 
   it('should check MIN correctly', function () {
-    var v = string().min(5);
-    var obj = object({
+    let v = string().min(5);
+    let obj = object({
       len: number(),
       name: string().min(ref('len')),
     });
@@ -153,8 +151,8 @@ describe('String types', () => {
   });
 
   it('should check MAX correctly', function () {
-    var v = string().max(5);
-    var obj = object({
+    let v = string().max(5);
+    let obj = object({
       len: number(),
       name: string().max(ref('len')),
     });
@@ -172,8 +170,8 @@ describe('String types', () => {
   });
 
   it('should check LENGTH correctly', function () {
-    var v = string().length(5);
-    var obj = object({
+    let v = string().length(5);
+    let obj = object({
       len: number(),
       name: string().length(ref('len')),
     });
@@ -191,7 +189,7 @@ describe('String types', () => {
   });
 
   it('should check url correctly', function () {
-    var v = string().url();
+    let v = string().url();
 
     return Promise.all([
       expect(v.isValid('//www.github.com/')).resolves.toBe(true),
@@ -201,7 +199,7 @@ describe('String types', () => {
   });
 
   it('should check UUID correctly', function () {
-    var v = string().uuid();
+    let v = string().uuid();
 
     return Promise.all([
       expect(v.isValid('0c40428c-d88d-4ff0-a5dc-a6755cb4f4d1')).resolves.toBe(
diff --git a/test/types/types.ts b/test/types/types.ts
index a35dc22..3807dff 100644
--- a/test/types/types.ts
+++ b/test/types/types.ts
@@ -1,43 +1,130 @@
-/* eslint-disable @typescript-eslint/no-unused-vars */
 /* eslint-disable @typescript-eslint/no-unused-expressions */
 /* eslint-disable no-unused-labels */
-/* eslint-disable @typescript-eslint/no-unused-expressions */
+import { array, number, string, date, ref, mixed } from '../../src';
+import { create as lazy } from '../../src/Lazy';
+import ObjectSchema, { create as object } from '../../src/object';
 
-import {
-  array,
-  string,
-  object,
-  mixed,
-  number,
-  ref,
-  lazy,
-  SchemaOf,
-  date,
-  BaseSchema,
-  StringSchema,
-} from '../../src';
-import type {
-  AssertsShape,
-  DefaultFromShape,
-  TypeOfShape,
-} from '../../src/object';
-import type { Config, ResolveFlags } from '../../src/schema';
-import { AnyObject, Preserve } from '../../src/types';
 import { _ } from '../../src/util/types';
 
-mixed().required().nullable();
+Mixed: {
+  const mxRequired = mixed<string | number>().required();
 
-string().required().nullable();
+  // $ExpectType string | number
+  mxRequired.cast(undefined);
 
-/** Type utils */
-{
-  const strRequired = string().required();
+  // $ExpectType string | number | null
+  mxRequired.nullable().cast(undefined);
+
+  // $ExpectType string | number
+  mxRequired.nullable().nonNullable().cast(undefined);
 
-  // string().default('hi').cast();
+  //
+  const mxOptional = mixed<string>().optional();
+
+  // $ExpectType string | undefined
+  mxOptional.cast(undefined);
+
+  // $ExpectType string
+  mxOptional.defined().cast(undefined);
+
+  //
+  const mxNullableOptional = mixed<string>().nullable().optional();
+
+  // $ExpectType string | null | undefined
+  mxNullableOptional.cast('');
+
+  // $ExpectType string
+  mxNullableOptional.required().validateSync('');
+
+  //
+  const mxNullable = mixed<string>().nullable();
+
+  // $ExpectType string | null | undefined
+  mxNullable.validateSync('');
+
+  const mxDefined = mixed<string>().default('');
+
+  // $ExpectType ""
+  mxDefined.getDefault();
+
+  const mxDefault = mixed<string>().nullable().default('').nullable();
+
+  // $ExpectType string | null
+  mxDefault.cast('');
+
+  // $ExpectType string | null
+  mxDefault.validateSync('');
+
+  // $ExpectType MixedSchema<string, AnyObject, "", "d">
+  const mxDefaultRequired = mixed<string>().nullable().required().default('');
+
+  // $ExpectType string
+  mxDefaultRequired.cast('');
+
+  // $ExpectType string
+  mxDefaultRequired.validateSync(null);
+
+  // $ExpectType "foo" | "bar"
+  string<'foo' | 'bar'>().defined().validateSync('foo');
+
+  // $ExpectType never
+  mixed<string>().strip().cast(undefined);
+
+  // $ExpectType string | undefined
+  mixed<string>().strip().strip(false).cast(undefined);
+
+  // $ExpectType string | undefined
+  mixed<string>().optional().concat(mixed<string>()).cast('');
+
+  // $ExpectType string
+  mixed<string>().optional().concat(mixed<string>().defined()).cast('');
+
+  // $ExpectType string | undefined
+  mixed<string>().nullable().concat(mixed<string>()).cast('');
+
+  // $ExpectType string | null | undefined
+  mixed<string>()
+    .nullable()
+    .concat(mixed<string>().optional().nullable())
+    .cast('');
+
+  // $ExpectType "foo" | undefined
+  mixed<string>().notRequired().concat(string<'foo'>()).cast('');
+
+  // $ExpectType "foo" | null
+  string<'foo'>()
+    .notRequired()
+    .concat(string().nullable().default('bar'))
+    .cast('');
+
+  // $ExpectType never
+  string<'bar'>().concat(string<'foo'>().defined()).cast('');
+
+  // $ExpectType never
+  string<'bar'>().concat(string<'foo'>()).cast('');
+}
+
+Strings: {
+  const strRequired = string().required();
 
   // $ExpectType string
   strRequired.cast(undefined);
 
+  // $ExpectType string | null
+  strRequired.nullable().cast(undefined);
+
+  // $ExpectType string
+  strRequired.nullable().nonNullable().cast(undefined);
+
+  //
+  const strOptional = string().optional();
+
+  // $ExpectType string | undefined
+  strOptional.cast(undefined);
+
+  // $ExpectType string
+  strOptional.defined().cast(undefined);
+
   //
   const strNullableOptional = string().nullable().optional();
 
@@ -47,7 +134,6 @@ string().required().nullable();
   // $ExpectType string
   strNullableOptional.required().validateSync('');
 
-  //
   //
   const strNullable = string().nullable();
 
@@ -56,8 +142,8 @@ string().required().nullable();
 
   const strDefined = string().default('');
 
-  // $ExpectType string
-  const _strDefined = strDefined.getDefault();
+  // $ExpectType ""
+  strDefined.getDefault();
 
   const strDefault = string().nullable().default('').nullable().trim();
 
@@ -67,8 +153,7 @@ string().required().nullable();
   // $ExpectType string | null
   strDefault.validateSync('');
 
-  //
-  //
+  // $ExpectType StringSchema<string, AnyObject, "", "d">
   const strDefaultRequired = string().nullable().required().default('').trim();
 
   // $ExpectType string
@@ -76,244 +161,268 @@ string().required().nullable();
 
   // $ExpectType string
   strDefaultRequired.validateSync(null);
-}
 
-{
-  const obj = object({
-    string: string<'foo'>().defined(),
-    number: number().default(1),
-    removed: number().strip(),
-    ref: ref('string'),
-    nest: object({
-      other: string(),
-    }),
-    lazy: lazy(() => number().defined()),
-  });
+  // $ExpectType "foo" | "bar"
+  string<'foo' | 'bar'>().defined().validateSync('foo');
 
-  type _d1 = DefaultFromShape<typeof obj['fields']>;
+  // $ExpectType never
+  string().strip().cast(undefined);
 
-  // $ExpectType number | undefined
-  type _i1 = TypeOfShape<typeof obj['fields']>['number'];
+  // $ExpectType string | undefined
+  string().strip().strip(false).cast(undefined);
 
-  // $ExpectType "foo"
-  type _i2 = TypeOfShape<typeof obj['fields']>['string'];
+  // $ExpectType string | undefined
+  string().optional().concat(string()).cast('');
+
+  // $ExpectType string
+  string().optional().concat(string().defined()).cast('');
 
-  // $ExpectType unknown
-  type _i3 = TypeOfShape<typeof obj['fields']>['ref'];
+  // $ExpectType string | undefined
+  string().nullable().concat(string()).cast('');
+
+  // $ExpectType string | null | undefined
+  string().nullable().concat(string().optional().nullable()).cast('');
+
+  // $ExpectType "foo" | undefined
+  string().notRequired().concat(string<'foo'>()).cast('');
+
+  // $ExpectType "foo" | null
+  string<'foo'>()
+    .notRequired()
+    .concat(string().nullable().default('bar'))
+    .cast('');
+
+  // $ExpectType never
+  string<'bar'>().concat(string<'foo'>().defined()).cast('');
+
+  // $ExpectType never
+  string<'bar'>().concat(string<'foo'>()).cast('');
+}
+
+Numbers: {
+  const numRequired = number().required();
 
   // $ExpectType number
-  type _i33 = TypeOfShape<typeof obj['fields']>['lazy'];
+  numRequired.cast(undefined);
+
+  // $ExpectType number | null
+  numRequired.nullable().cast(undefined);
 
   // $ExpectType number
-  type _i4 = AssertsShape<typeof obj['fields']>['number'];
+  numRequired.nullable().nonNullable().cast(undefined);
 
-  // $ExpectType "foo"
-  type _i5 = AssertsShape<typeof obj['fields']>['string'];
+  //
+  const numOptional = number().optional();
 
-  type _i6 = _<AssertsShape<typeof obj['fields']>>;
+  // $ExpectType number | undefined
+  numOptional.cast(undefined);
 
   // $ExpectType number
-  type _i7 = AssertsShape<typeof obj['fields']>['lazy'];
-
-  const cast1 = obj.cast({});
+  numOptional.defined().cast(undefined);
 
-  // $ExpectType string | undefined
-  cast1!.nest!.other;
+  //
+  const numNullableOptional = number().nullable().optional();
 
-  // $ExpectType "foo"
-  cast1!.string;
+  // $ExpectType number | null | undefined
+  numNullableOptional.cast('');
 
   // $ExpectType number
-  cast1!.number;
+  numNullableOptional.required().validateSync('');
 
   //
-  // Object Defaults
   //
-  const dflt1 = obj.getDefaultFromShape();
+  const numNullable = number().nullable();
 
-  // $ExpectType number
-  dflt1.number;
+  // $ExpectType number | null | undefined
+  numNullable.validateSync('');
 
-  // $ExpectType undefined
-  dflt1.ref;
+  const numDefined = number().default(3);
 
-  // $ExpectType undefined
-  dflt1.lazy;
+  // $ExpectType 3
+  numDefined.getDefault();
 
-  // $ExpectType undefined
-  dflt1.string;
+  const numDefault = number().nullable().default(3).nullable().min(2);
 
-  // $ExpectType undefined
-  dflt1.nest.other;
+  // $ExpectType number | null
+  numDefault.cast('');
 
-  const merge = object({
-    field: string().required(),
-    other: string().default(''),
-  }).shape({
-    field: number(),
-  });
+  // $ExpectType number | null
+  numDefault.validateSync('');
+
+  //
+  const numDefaultRequired = number().nullable().required().default(3);
+
+  // $ExpectType number
+  numDefaultRequired.cast('');
+
+  // $ExpectType number
+  numDefaultRequired.validateSync(null);
+
+  // $ExpectType never
+  number().strip().cast(undefined);
 
   // $ExpectType number | undefined
-  merge.cast({}).field;
+  number().strip().strip(false).cast(undefined);
 
-  // $ExpectType string
-  merge.cast({}).other;
+  // $ExpectType 1 | undefined
+  number().notRequired().concat(number<1>()).cast('');
+
+  // $ExpectType 1 | null
+  number<1>().notRequired().concat(number().nullable().default(2)).cast('');
+
+  // $ExpectType never
+  number<2>().concat(number<1>().defined()).cast('');
+
+  // $ExpectType never
+  number<2>().concat(number<1>()).cast('');
 }
 
-ObjectPartial: {
-  const schema = object({
-    // age: number(),
-    name: string().required(),
-    address: object()
-      .shape({
-        line1: string().required(),
-        zip: number().required(),
-      })
-      .default(undefined),
-  }).nullable();
+date: {
+  const dtRequired = date().required();
 
-  const partial = schema.partial();
+  // $ExpectType Date
+  dtRequired.cast(undefined);
 
-  // $ExpectType string | undefined
-  partial.validateSync({ age: '1' })!.name;
+  // $ExpectType Date | null
+  dtRequired.nullable().cast(undefined);
 
-  // $ExpectType StringSchema<string | undefined, Config<AnyObject, "">>
-  partial.fields.name;
+  // $ExpectType Date
+  dtRequired.nullable().nonNullable().cast(undefined);
 
-  // $ExpectType string
-  partial.validateSync({})!.address!.line1;
+  //
+  const dtOptional = date().optional();
 
-  const deepPartial = schema.deepPartial();
+  // $ExpectType Date | undefined
+  dtOptional.cast(undefined);
 
-  // $ExpectType string | undefined
-  deepPartial.validateSync({ age: '1' })!.name;
+  // $ExpectType Date
+  dtOptional.defined().cast(undefined);
 
-  // $ExpectType string | undefined
-  deepPartial.validateSync({})!.address!.line1;
-}
+  //
+  const dtNullableOptional = date().nullable().optional();
 
-ObjectPick: {
-  const schema = object({
-    age: number(),
-    name: string().required(),
-  })
+  // $ExpectType Date | null | undefined
+  dtNullableOptional.cast('');
+
+  // $ExpectType Date
+  dtNullableOptional.required().validateSync('');
+
+  //
+  //
+  const dtNullable = date().nullable();
+
+  // $ExpectType Date | null | undefined
+  dtNullable.validateSync('');
+
+  const dtDefined = date().default(() => new Date());
+
+  // $ExpectType Date
+  const _dtDefined = dtDefined.getDefault();
+
+  const dtDefault = date()
+    .nullable()
+    .default(() => new Date())
     .nullable()
-    .required();
+    .min(new Date());
 
-  // $ExpectType number | undefined
-  schema.pick(['age']).validateSync({ age: '1' }).age;
-}
+  // $ExpectType Date | null
+  dtDefault.cast('');
 
-ObjectOmit: {
-  const schema = object({
-    age: number(),
-    name: string().required(),
-  })
+  // $ExpectType Date | null
+  dtDefault.validateSync('');
+
+  //
+  const dtDefaultRequired = date()
     .nullable()
-    .required();
+    .required()
+    .default(() => new Date());
 
-  // $ExpectType string
-  schema.omit(['age']).validateSync({ name: '1' }).name;
+  // $ExpectType Date
+  dtDefaultRequired.cast('');
 
-  // $ExpectType string | undefined
-  schema.omit(['age']).partial().validateSync({ name: '1' }).name;
+  // $ExpectType Date
+  dtDefaultRequired.validateSync(null);
+
+  // $ExpectType never
+  date().strip().cast(undefined);
+
+  // $ExpectType Date | undefined
+  date().strip().strip(false).cast(undefined);
 }
 
-SchemaOf: {
-  type Person = {
-    nested?: {
-      name: string;
-    };
-    firstName: string;
-    title: string | undefined;
-    age?: number;
-    colors: string[];
-    createdAt: Date;
-  };
-
-  type PersonSchema = SchemaOf<Person>;
-  const _b: BaseSchema<
-    string,
-    Config<AnyObject, ''>
-  > = null as any as StringSchema<string, Config<AnyObject, ''>>;
-
-  const _t: PersonSchema = object({
-    firstName: string().defined(),
-    title: string(),
-    age: lazy(() => number()),
-    colors: array(string().defined()),
-    createdAt: date().defined(),
-    nested: object({
-      name: string().required(),
-    }),
-  });
+Lazy: {
+  const l = lazy(() => string().default('asfasf'));
+
+  l.cast(null);
 }
 
-SchemaOfDate: {
-  type Employee = {
-    hire_date: Date;
-    name: string;
-  };
+Array: {
+  const arrRequired = array().required();
 
-  type EmployeeSchema = SchemaOf<Employee>;
+  // $ExpectType any[]
+  arrRequired.cast(undefined);
 
-  const _t: EmployeeSchema = object({
-    name: string().defined(),
-    hire_date: date().defined(),
-  });
-}
+  // $ExpectType any[] | null
+  arrRequired.nullable().cast(undefined);
 
-SchemaOfDateArray: {
-  type EmployeeWithPromotions = {
-    promotion_dates: Date[];
-    name: string;
-  };
+  // $ExpectType any[]
+  arrRequired.nullable().nonNullable().cast(undefined);
 
-  type EmployeeWithPromotionsSchema = SchemaOf<EmployeeWithPromotions>;
+  //
+  const arrOptional = array().optional();
 
-  const _t: EmployeeWithPromotionsSchema = object({
-    name: string().defined(),
-    promotion_dates: array().of(date().defined()),
-  });
-}
+  // $ExpectType any[] | undefined
+  arrOptional.cast(undefined);
 
-SchemaOfFile: {
-  type Document = {
-    file: File;
-    date_uploaded: Date;
-    notes: string;
-  };
+  // $ExpectType any[]
+  arrOptional.defined().cast(undefined);
 
-  type FileSchema = SchemaOf<Document, File>;
+  //
+  const arrNullableOptional = array().nullable().optional();
 
-  const _t: FileSchema = object({
-    file: mixed<File>().defined(),
-    date_uploaded: date().defined(),
-    notes: string().defined(),
-  });
-}
+  // $ExpectType any[] | null | undefined
+  arrNullableOptional.cast('');
 
-SchemaOfFileArray: {
-  type DocumentWithFullHistory = {
-    history: File[];
-    name: string;
-  };
+  // $ExpectType any[]
+  arrNullableOptional.required().validateSync('');
 
-  type DocumentWithFullHistorySchema = SchemaOf<DocumentWithFullHistory, File>;
+  //
+  //
+  const arrNullable = array().nullable();
 
-  const _t: DocumentWithFullHistorySchema = object({
-    name: string().defined(),
-    history: array().of(mixed<File>().defined()),
-  });
-}
+  // $ExpectType any[] | null | undefined
+  arrNullable.validateSync('');
+
+  const arrDefined = array().default(() => [] as unknown[]);
+
+  // $ExpectType unknown[]
+  arrDefined.getDefault();
+
+  const arrDefault = array()
+    .optional()
+    .default(() => [] as unknown[])
+    .nullable()
+    .min(1);
+
+  // $ExpectType any[] | null
+  arrDefault.cast('');
+
+  // $ExpectType any[] | null
+  arrDefault.validateSync('');
+
+  //
+  const arrDefaultRequired = array()
+    .nullable()
+    .required()
+    .default(() => [] as unknown[]);
 
-{
-  // const str = string();
-  // type f = Type<typeof str>;
+  // $ExpectType any[]
+  arrDefaultRequired.cast('');
+
+  // $ExpectType any[]
+  arrDefaultRequired.validateSync(null);
 
-  type _b = Preserve<Config<any, '' | 's'>['flags'], 'd'>;
-  // type _a = HasFlag<Config<any, '' | 's'>['flags'], 'd'>;
-  type _f = ResolveFlags<string | undefined, Config<any, ''>['flags']>;
+  array().default<string[]>(() => []);
 
   // $ExpectType (string | undefined)[] | undefined
   array(string()).cast(null);
@@ -333,7 +442,7 @@ SchemaOfFileArray: {
   // $ExpectType (string | null)[] | undefined
   array(string().nullable().default('')).validateSync(null);
 
-  // $ExpectType any[]
+  // $ExpectType number[]
   array()
     .default([] as number[])
     .getDefault();
@@ -349,9 +458,77 @@ SchemaOfFileArray: {
 
   const numList = [1, 2];
 
-  // $ExpectType (number | undefined)[]
+  // $ExpectType number[]
   array(number()).default(numList).getDefault();
 
+  // $ExpectType (number | undefined)[]
+  array(number()).concat(array(number()).required()).validateSync([]);
+
+  // $ExpectType never
+  array().strip().cast(undefined);
+
+  // $ExpectType any[] | undefined
+  array().strip().strip(false).cast(undefined);
+
+  ArrayConcat: {
+    const arrReq = array(number()).required();
+
+    // $ExpectType (number | undefined)[]
+    const _c1 = array(number()).concat(arrReq).validateSync([]);
+  }
+}
+
+Object: {
+  const objRequired = object().required();
+
+  // $ExpectType {}
+  objRequired.cast(undefined);
+
+  // $ExpectType {} | null
+  objRequired.nullable().cast(undefined);
+
+  // $ExpectType {}
+  objRequired.nullable().nonNullable().cast(undefined);
+
+  //
+  const objOptional = object().optional();
+
+  // $ExpectType {}
+  objOptional.cast(undefined);
+
+  // $ExpectType {}
+  objOptional.defined().cast(undefined);
+
+  //
+  const objNullableOptional = object().nullable().optional();
+
+  // $ExpectType {} | null
+  objNullableOptional.cast('');
+
+  // $ExpectType {}
+  objNullableOptional.required().validateSync('');
+
+  //
+  //
+  const objNullable = object().nullable();
+
+  // $ExpectType {} | null
+  objNullable.validateSync('');
+
+  const v = object({
+    name: string().defined(),
+    colors: array(string().defined()).required(),
+  }).nullable();
+
+  // $ExpectType { name: string; colors: string[]; } | null
+  v.cast({});
+
+  interface Person {
+    name: string;
+  }
+
+  const _person: ObjectSchema<Person> = object({ name: string().defined() });
+
   const a1 = object({
     list: array(number().required()).required(),
     nested: array(
@@ -372,75 +549,174 @@ SchemaOfFileArray: {
   // $ExpectType string
   a1.nested![0].name;
 
-  // $ExpectType (number | undefined)[]
-  const _c1 = array(number())
-    .concat(array(number()).required())
-    .validateSync([]);
+  const obj = object({
+    string: string<'foo'>().defined(),
+    number: number().default(1),
+    removed: number().strip(),
+    ref: ref('string'),
+    nest: object({
+      other: string(),
+    }),
+    nullObject: object({
+      other: string(),
+    }).default(null),
+    lazy: lazy(() => number().defined()),
+  });
 
-  // $ExpectType { [x: string]: any; a: number; }[] | null
-  const _definedArray: Array<{ a: number }> | null = array()
-    .of(object({ a: number().required() }))
-    .nullable()
-    .defined()
-    .validateSync([]);
+  const cast1 = obj.cast({});
 
-  // $ExpectType { [x: string]: any; a: number; }[]
-  const _requiredArray: Array<{ a: number }> = array()
-    .of(object({ a: number().required() }))
-    .nullable()
-    .required()
-    .validateSync([]);
-}
+  let _f = obj.getDefault();
 
-{
   // $ExpectType string | undefined
-  lazy(() => string()).cast(3);
+  cast1!.nest!.other;
 
-  // $ExpectType string | number | undefined
-  lazy((v) => (typeof v === 'string' ? string() : number())).cast(3);
-}
+  // $ExpectType "foo"
+  cast1!.string;
 
-//
-// CONCAT
-//
-{
-  // $ExpectType string
-  mixed<string>().concat(mixed<string>().required()).validateSync('');
+  // $ExpectType number
+  cast1!.number;
 
-  // $ExpectType string | number | undefined
-  const _oo = mixed<number>()
-    .required()
-    .concat(mixed<string>())
-    .validateSync('');
-
-  const _o = object({
-    str: string(),
-  }).concat(
-    object({
-      name: string(),
-    }),
-  );
+  // $ExpectType never
+  object().strip().cast(undefined);
 
-  // $ExpectType string
-  string().nullable().default('hi').concat(string()).cast('');
+  // $ExpectType {}
+  object().strip().strip(false).cast(undefined);
+
+  //
+  // Object Defaults
+  //
+  const dflt1 = obj.getDefault();
+
+  // $ExpectType 1
+  dflt1.number;
+
+  // $ExpectType undefined
+  dflt1.ref;
+
+  // $ExpectType undefined
+  dflt1.lazy;
+
+  // $ExpectType undefined
+  dflt1.string;
+
+  // $ExpectType undefined
+  dflt1.nest.other;
+
+  // $ExpectType null
+  dflt1.nullObject;
+
+  const merge = object({
+    field: string().required(),
+    other: string().default(''),
+  }).shape({
+    field: number().default(1),
+    name: string(),
+  });
+
+  // $ExpectType { name?: string | undefined; other: string; field: number; }
+  merge.cast({});
 
   // $ExpectType number
-  number().nullable().default(1).concat(number()).cast('');
+  merge.cast({}).field;
 
-  // $ExpectType string | null
-  string().default('hi').concat(string().nullable()).cast('');
+  // $ExpectType string
+  merge.cast({}).other;
 
-  // $ExpectType number | null
-  number().default(0).concat(number().nullable()).cast('');
-}
+  SchemaOfDate: {
+    type Employee = {
+      hire_date: Date;
+      name: string;
+    };
+
+    const _t: ObjectSchema<Employee> = object({
+      name: string().defined(),
+      hire_date: date().defined(),
+    });
+  }
+
+  SchemaOfDateArray: {
+    type EmployeeWithPromotions = {
+      promotion_dates: Date[];
+      name: string;
+    };
+
+    const _t: ObjectSchema<EmployeeWithPromotions> = object({
+      name: string().defined(),
+      promotion_dates: array().of(date().defined()).defined(),
+    });
+  }
 
-// Context: {
-//   type Context = { isCool: boolean };
+  SchemaOfFileArray: {
+    type DocumentWithFullHistory = {
+      history?: File[];
+      name: string;
+    };
+
+    const _t: ObjectSchema<DocumentWithFullHistory> = object({
+      name: string().defined(),
+      history: array().of(mixed<File>().defined()),
+    });
+  }
+
+  ObjectPick: {
+    const schema = object({
+      age: number(),
+      name: string().required(),
+    })
+      .nullable()
+      .required();
+
+    // $ExpectType number | undefined
+    schema.pick(['age']).validateSync({ age: '1' }).age;
+
+    // $ExpectType { age?: number | undefined; }
+    schema.pick(['age']).validateSync({ age: '1' });
+  }
+
+  ObjectOmit: {
+    const schema = object({
+      age: number(),
+      name: string().required(),
+    })
+      .nullable()
+      .required();
+
+    // $ExpectType string
+    schema.omit(['age']).validateSync({ name: '1' }).name;
 
-//   const schema = object({
-//     str: string().when('$isCool', {
-//       is: true,
-//       then: string().required(),
-//     }),
-//   });
-// }
+    // $ExpectType string | undefined
+    schema.omit(['age']).partial().validateSync({ name: '1' }).name;
+
+    // $ExpectType { name: string; }
+    schema.omit(['age']).validateSync({ name: '1' });
+  }
+
+  ObjectPartial: {
+    const schema = object({
+      // age: number(),
+      name: string().required(),
+      address: object()
+        .shape({
+          line1: string().required(),
+          zip: number().required().strip(),
+        })
+        .default(undefined),
+    }).nullable();
+
+    const partial = schema.partial();
+
+    // $ExpectType string | undefined
+    partial.validateSync({ age: '1' })!.name;
+
+    // $ExpectType string
+    partial.validateSync({})!.address!.line1;
+
+    const deepPartial = schema.deepPartial();
+
+    // $ExpectType string | undefined
+    deepPartial.validateSync({ age: '1' })!.name;
+
+    // $ExpectType string | undefined
+    deepPartial.validateSync({})!.address!.line1;
+  }
+}
diff --git a/test/yup.js b/test/yup.js
index afaef10..4621f71 100644
--- a/test/yup.js
+++ b/test/yup.js
@@ -9,7 +9,6 @@ import {
   number,
   boolean,
   date,
-  ValidationError,
   ObjectSchema,
   ArraySchema,
   StringSchema,
@@ -28,10 +27,12 @@ describe('Yup', function () {
   it('cast should assert on undefined cast results', () => {
     expect(() =>
       string()
-        .toBeDefined()
+        .defined()
         .transform(() => undefined)
         .cast('foo'),
-    ).toThrowError();
+    ).toThrowError(
+      'The value of field could not be cast to a value that satisfies the schema type: "string".',
+    );
   });
 
   it('cast should respect assert option', () => {
@@ -113,7 +114,7 @@ describe('Yup', function () {
   });
 
   it('should REACH conditionally correctly', async function () {
-    var num = number().oneOf([4]),
+    let num = number().oneOf([4]),
       inst = object().shape({
         num: number().max(4),
         nested: object().shape({
@@ -172,7 +173,7 @@ describe('Yup', function () {
 
     await expect(
       reached.validate(5, { context, parent: { foo: 5 } }),
-    ).rejects.toThrowError(ValidationError, /one of the following/);
+    ).rejects.toThrowError(/one of the following/);
   });
 
   it('should reach through lazy', async () => {