Repository: jquense/yup
Pull request #453 (2019-02-07 21:40:00)

fix: default message for test with object
Two things:

1) Fix. If you use `test()` with multiply arguments like `test('name', func)` then you will be provided with default message (`locale.default`). But not if you use it with `opts` object without message `test(opts)` or with second argument as `undefined` `test('name', undefined, func)` (it is assumed right now that since there are 3 arguments then message is present). Now all variants treated equally and if message is `undefined` then it will be set to `locale.default`.

2) Small feature. Now it is possible to write `test(func)`. Since `name` is needed only for exclusive tests it can be omitted (like it is possible to omit it in `opts` form).


Patch:
diff --git a/README.md b/README.md
index ec15a98..4822a55 100644
--- a/README.md
+++ b/README.md
@@ -123,14 +123,14 @@ When using es module syntax, yup exports everything as a named export
 
 ```js
 import * as yup from 'yup'; // for everything
-// or only what you need
-import { string, object } from 'yup';
+// or
+import { string, object } from 'yup'; // for only what you need
 ```
 
 ```js
-var yup = require('yup');
+let yup = require('yup');
 
-var schema = yup.object().shape({
+let schema = yup.object().shape({
   name: yup.string().required(),
   age: yup
     .number()
@@ -144,7 +144,7 @@ var schema = yup.object().shape({
   }),
 });
 
-//check validity
+// check validity
 schema
   .isValid({
     name: 'jimmy',
@@ -154,7 +154,7 @@ schema
     valid; // => true
   });
 
-//you can try and type cast objects to the defined schema
+// you can try and type cast objects to the defined schema
 schema.cast({
   name: 'jimmy',
   age: '24',
@@ -182,13 +182,14 @@ setLocale({
   },
 });
 
-// Now use Yup schemas AFTER you defined your custom dictionary
-const schema = yup.object().shape({
+// now use Yup schemas AFTER you defined your custom dictionary
+let schema = yup.object().shape({
   name: yup.string(),
   age: yup.number().min(18),
 });
+
 schema.validate({ name: 'jimmy', age: 11 }).catch(function(err) {
-  err.name; // 'ValidationError'
+  err.name; // => 'ValidationError'
   err.errors; // => ['Deve ser maior que 18']
 });
 ```
@@ -200,7 +201,7 @@ schema.validate({ name: 'jimmy', age: 11 }).catch(function(err) {
 The module export.
 
 ```js
-var yup = require('yup');
+let yup = require('yup');
 
 yup.mixed;
 yup.string;
@@ -212,6 +213,9 @@ yup.array;
 
 yup.reach;
 yup.addMethod;
+yup.ref;
+yup.lazy;
+yup.setLocale;
 yup.ValidationError;
 ```
 
@@ -223,7 +227,7 @@ For nested schema that need to resolve dynamically, you can provide a `value` an
 a `context` object.
 
 ```js
-var schema = object().shape({
+let schema = object().shape({
   nested: object().shape({
     arr: array().of(object().shape({ num: number().max(4) })),
   }),
@@ -258,16 +262,16 @@ at _validation/cast time_ and supported where specified. Ref's are evaluated in
 the ref value is resolved before the field using the ref (be careful of circular dependencies!).
 
 ```js
-var schema = object({
+let schema = object({
   baz: ref('foo.bar'),
   foo: object({
-    bar: string()
-  })
-  x: ref('$x')
-})
+    bar: string(),
+  }),
+  x: ref('$x'),
+});
 
-inst.cast({ foo: { bar: 'boom' } }, { context: { x: 5 } })
-// { baz: 'boom',  x: 5, { foo: { bar: 'boom' } }, }
+schema.cast({ foo: { bar: 'boom' } }, { context: { x: 5 } });
+// => { baz: 'boom',  x: 5, foo: { bar: 'boom' } }
 ```
 
 #### `yup.lazy((value: any) => Schema): Lazy`
@@ -279,7 +283,7 @@ recursive schema like Trees, for polymophic fields and arrays.
 to `undefined` on the child otherwise the object will infinitely nest itself when you cast it!.
 
 ```js
-var node = object({
+let node = object({
   id: number(),
   child: yup.lazy(() => node.default(undefined)),
 });
@@ -313,10 +317,11 @@ Thrown on failed validations, with the following properties
 
 Creates a schema that matches all types. All types inherit from this base type
 
-```javascript
-var schema = yup.mixed();
+```js
+let schema = yup.mixed();
+
 schema.isValid(undefined, function(valid) {
-  valid; //=> true
+  valid; // => true
 });
 ```
 
@@ -384,7 +389,7 @@ schema.validate({ name: 'jimmy', age: 24 }).then(function(value) {
 });
 
 schema.validate({ name: 'jimmy', age: 'hi' }).catch(function(err) {
-  err.name; // 'ValidationError'
+  err.name; // => 'ValidationError'
   err.errors; // => ['age must be a number']
 });
 ```
@@ -398,7 +403,7 @@ Synchronous validation only works if there are no configured async tests, e.g te
 For instance this will work:
 
 ```js
-schema = number.test(
+let schema = number().test(
   'is-42',
   "this isn't the number i want",
   value => value != 42,
@@ -410,7 +415,7 @@ schema.validateSync(23); // throws ValidationError
 however this will not:
 
 ```js
-schema = number.test('is-42', "this isn't the number i want", value =>
+let schema = number().test('is-42', "this isn't the number i want", value =>
   Promise.resolve(value != 42),
 );
 
@@ -425,10 +430,10 @@ but uses the resulting schema as the subject for validation.
 > Note! The `value` here is the _root_ value relative to the starting schema, not the value at the nested path.
 
 ```js
-const schema = object({
+let schema = object({
   foo: array().of(
     object({
-      loose: bool(),
+      loose: boolean(),
       bar: string().when('loose', {
         is: true,
         otherwise: s => s.strict(),
@@ -437,13 +442,13 @@ const schema = object({
   ),
 });
 
-const rootValue = {
+let rootValue = {
   foo: [{ bar: 1 }, { bar: 1, loose: true }],
 };
 
-await schema.validateAt('foo[0].bar', rootValue); // -> '1';
+await schema.validateAt('foo[0].bar', rootValue); // => ValidationError: must be a string
 
-await schema.validateAt('foo[1].bar', rootValue); // -> ValidationError. must be a string;
+await schema.validateAt('foo[1].bar', rootValue); // => '1'
 ```
 
 #### `mixed.validateSyncAt(path: string, value: any, options?: object): any`
@@ -492,7 +497,7 @@ let schema = object({
   notThis: string().strip(),
 });
 
-schema.cast({ notThis: 'foo', useThis: 4 }); // { useThis: 4 }
+schema.cast({ notThis: 'foo', useThis: 4 }); // => { useThis: 4 }
 ```
 
 #### `mixed.withMutation(builder: (current: Schema) => void): void`
@@ -534,7 +539,7 @@ yup.object.default({ number: 5 }); // object will be cloned every time a default
 
 yup.object.default(() => ({ number: 5 })); // this is cheaper
 
-yup.date.default(() => new Date()); //also helpful for defaults that change over time
+yup.date.default(() => new Date()); // also helpful for defaults that change over time
 ```
 
 #### `mixed.default(): Any`
@@ -564,11 +569,12 @@ be used in the `message` argument.
 Whitelist a set of values. Values added are automatically removed from any blacklist if they are in it.
 The `${values}` interpolation can be used in the `message` argument.
 
-```javascript
-var schema = yup.mixed().oneOf(['jimmy', 42]);
-schema.isValid(42); //=> true
-schema.isValid('jimmy'); //=> true
-schema.isValid(new Date()); //=> false
+```js
+let schema = yup.mixed().oneOf(['jimmy', 42]);
+
+await schema.isValid(42); // => true
+await schema.isValid('jimmy'); // => true
+await schema.isValid(new Date()); // => false
 ```
 
 #### `mixed.notOneOf(arrayOfValues: Array<any>, message?: string | function)`
@@ -576,10 +582,11 @@ schema.isValid(new Date()); //=> false
 Blacklist a set of values. Values added are automatically removed from any whitelist if they are in it.
 The `${values}` interpolation can be used in the `message` argument.
 
-```javascript
-var schema = yup.mixed().notOneOf(['jimmy', 42]);
-schema.isValid(42); //=> false
-schema.isValid(new Date()); //=> true
+```js
+let schema = yup.mixed().notOneOf(['jimmy', 42]);
+
+await schema.isValid(42); // => false
+await schema.isValid(new Date()); // => true
 ```
 
 #### `mixed.when(keys: string | Array<string>, builder: object | (value, schema)=> Schema): Schema`
@@ -594,11 +601,10 @@ can provide a function like: `is: (value) => value == true`.
 Like joi you can also prefix properties with `$` to specify a property that is dependent
 on `context` passed in by `validate()` or `isValid`. `when` conditions are additive.
 
-```javascript
-var inst = yup.object({
-  isBig: yup.boolean(),
-  count: yup
-    .number()
+```js
+let schema = object({
+  isBig: boolean(),
+  count: number()
     .when('isBig', {
       is: true, // alternatively: (val) => val == true
       then: yup.number().min(5),
@@ -607,23 +613,23 @@ var inst = yup.object({
     .when('$other', (other, schema) => (other === 4 ? schema.max(6) : schema)),
 });
 
-inst.validate(value, { context: { other: 4 } });
+await schema.validate(value, { context: { other: 4 } });
 ```
 
 You can also specify more than one dependent key, in which case each value will be spread as an argument.
 
-```javascript
-var inst = yup.object({
-  isSpecial: yup.bool(),
-  isBig: yup.bool(),
-  count: yup.number().when(['isBig', 'isSpecial'], {
+```js
+let schema = object({
+  isSpecial: boolean(),
+  isBig: boolean(),
+  count: number().when(['isBig', 'isSpecial'], {
     is: true, // alternatively: (isBig, isSpecial) => isBig && isSpecial
     then: yup.number().min(5),
     otherwise: yup.number().min(0),
   }),
 });
 
-inst.validate({
+await schema.validate({
   isBig: true,
   isSpecial: true,
   count: 10,
@@ -634,14 +640,14 @@ Alternatively you can provide a function that returns a schema
 (called with the value of the key and the current schema).
 
 ```js
-var inst = yup.object({
+let schema = yup.object({
   isBig: yup.boolean(),
   count: yup.number().when('isBig', (isBig, schema) => {
     return isBig ? schema.min(5) : schema.min(0);
   }),
 });
 
-inst.validate({ isBig: false, count: 4 });
+await schema.validate({ isBig: false, count: 4 });
 ```
 
 #### `mixed.test(name: string, message: string | function, test: function): Schema`
@@ -663,20 +669,21 @@ the `test` function is called with the current `value`. For more advanced valida
 use the alternate signature to provide more options (see below):
 
 ```js
-var jimmySchema = yup.string()
-  .test('is-jimmy', '${path} is not Jimmy', value => value === 'jimmy');
+let jimmySchema = string().test(
+  'is-jimmy',
+  '${path} is not Jimmy',
+  value => value === 'jimmy',
+);
 
 // or make it async by returning a promise
-var asyncJimmySchema = yup.string()
-  .test('is-jimmy', '${path} is not Jimmy', function (value){
-    return fetch('/is-jimmy/' + value)
-      .then(response => response.responseText === 'true')
-  });
-
-schema.isValid('jimmy').then(...) //=> true
+let asyncJimmySchema = string().test(
+  'is-jimmy',
+  '${path} is not Jimmy',
+  async (value) => (await fetch('/is-jimmy/' + value)).responseText === 'true',
+});
 
-schema.isValid('john').then(...) //=> false
-schema.errors // => [ 'this is not Jimmy!']
+await schema.isValid('jimmy'); // => true
+await schema.isValid('john'); // => false
 ```
 
 test functions are called with a special context, or `this` value, that exposes some useful metadata and functions. Note that to use the `this` context the test function must be a function expression (`function test(value) {}`), not an arrow function, since arrow functions have lexical context.
@@ -695,11 +702,11 @@ Alternative `test(..)` signature. `options` is an object containing some of the
 
 ```js
 Options = {
-  // Unique name identifying the test
+  // unique name identifying the test
   name: string;
   // test function, determines schema validity
   test: (value: any) => boolean;
-  // The validation error message
+  // the validation error message
   message: string;
   // values passed to message for interpolation
   params: ?object;
@@ -715,9 +722,9 @@ the exclusive test is removed and further tests of the same name will be stacked
 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.
 
-```javascript
-var max = 64;
-var schema = yup.mixed().test({
+```js
+let max = 64;
+let schema = yup.mixed().test({
   name: 'max',
   exclusive: true,
   params: { max },
@@ -736,12 +743,12 @@ Transformations are useful for arbitrarily altering how the object is cast, **ho
 not to mutate the passed in value.** Transforms are run sequentially so each `value` represents the
 current state of the cast, you can use the `originalValue` param if you need to work on the raw initial value.
 
-```javascript
-var schema = yup.string().transform(function(currentValue, originalvalue) {
+```js
+let schema = string().transform(function(value, originalvalue) {
   return this.isType(value) && value !== null ? value.toUpperCase() : value;
 });
 
-schema.cast('jimmy'); //=> 'JIMMY'
+schema.cast('jimmy'); // => 'JIMMY'
 ```
 
 Each types will handle basic coercion of values to the proper type for you, but occasionally
@@ -749,25 +756,28 @@ you may want to adjust or refine the default behavior. For example, if you wante
 date parsing strategy than the default one you could do that with a transform.
 
 ```js
-yup.date().transform(function(formats = 'MMM dd, yyyy') {
-  //check to see if the previous transform already parsed the date
-  if (this.isType(value)) return value;
+module.exports = function(formats = 'MMM dd, yyyy') {
+  return date().transform(function(value, originalvalue) {
+    // check to see if the previous transform already parsed the date
+    if (this.isType(value)) return value;
 
-  //the default coercion failed so lets try it with Moment.js instead
-  value = Moment(originalValue, formats);
+    // the default coercion failed so lets try it with Moment.js instead
+    value = Moment(originalValue, formats);
 
-  //if its valid return the date object, otherwise return an `InvalidDate`
-  return date.isValid() ? date.toDate() : new Date('');
-});
+    // if its valid return the date object, otherwise return an `InvalidDate`
+    return value.isValid() ? value.toDate() : new Date('');
+  });
+};
 ```
 
 ### string
 
 Define a string schema. Supports all the same methods as [`mixed`](#mixed).
 
-```javascript
-var schema = yup.string();
-schema.isValid('hello'); //=> true
+```js
+let schema = yup.string();
+
+await schema.isValid('hello'); // => true
 ```
 
 By default, the `cast` logic of `string` is to call `toString` on the value if it exists.
@@ -795,14 +805,11 @@ Set a maximum length limit for the string value. The `${max}` interpolation can
 
 Provide an arbitrary `regex` to match the value against.
 
-```javascript
-var v = string().matches(/(hi|bye)/);
-v.isValid('hi')
-  .should.eventually()
-  .equal(true);
-v.isValid('nope')
-  .should.eventually()
-  .equal(false);
+```js
+let schema = string().matches(/(hi|bye)/);
+
+await schema.isValid('hi'); // => true
+await schema.isValid('nope'); // => false
 ```
 
 #### `string.matches(regex: Regex, options: { message: string, excludeEmptyString: bool }): Schema`
@@ -810,11 +817,10 @@ v.isValid('nope')
 An alternate signature for `string.matches` with an options object. `excludeEmptyString`, when true,
 short circuits the regex test when the value is an empty string
 
-```javascript
-var v = string().matches(/(hi|bye)/, { excludeEmptyString: true });
-v.isValid('')
-  .should.eventually()
-  .equal(true);
+```js
+let schema = string().matches(/(hi|bye)/, { excludeEmptyString: true });
+
+await schema.isValid(''); // => true
 ```
 
 #### `string.email(message?: string | function): Schema`
@@ -849,9 +855,10 @@ will only validate that the value is uppercase.
 
 Define a number schema. Supports all the same methods as [`mixed`](#mixed).
 
-```javascript
-var schema = yup.number();
-schema.isValid(10); //=> true
+```js
+let schema = yup.number();
+
+await schema.isValid(10); // => true
 ```
 
 The default `cast` logic of `number` is: [`parseFloat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat).
@@ -903,9 +910,10 @@ Adjusts the value via the specified method of `Math` (defaults to 'round').
 
 Define a boolean schema. Supports all the same methods as [`mixed`](#mixed).
 
-```javascript
-var schema = yup.boolean();
-schema.isValid(true); //=> true
+```js
+let schema = yup.boolean();
+
+await schema.isValid(true); // => true
 ```
 
 ### date
@@ -914,9 +922,10 @@ Define a Date schema. By default ISO date strings will parse correctly,
 for more robust parsing options see the extending schema types at the end of the readme.
 Supports all the same methods as [`mixed`](#mixed).
 
-```javascript
-var schema = yup.date();
-schema.isValid(new Date()); //=> true
+```js
+let schema = yup.date();
+
+await schema.isValid(new Date()); // => true
 ```
 
 The default `cast` logic of `date` is pass the value to the `Date` constructor, failing that, it will attempt
@@ -940,19 +949,20 @@ Define an array schema. Arrays can be typed or not, When specifying the element
 will apply to the elements as well. Options passed into `isValid` are passed also passed to child schemas.
 Supports all the same methods as [`mixed`](#mixed).
 
-```javascript
-var schema = yup.array().of(yup.number().min(2));
-schema.isValid([2, 3]); //=> true
-schema.isValid([1, -24]); //=> false
+```js
+let schema = yup.array().of(yup.number().min(2));
+
+await schema.isValid([2, 3]); // => true
+await schema.isValid([1, -24]); // => false
 
-schema.cast(['2', '3']); //=> [2, 3]
+schema.cast(['2', '3']); // => [2, 3]
 ```
 
 You can also pass a subtype schema to the array constructor as a convenience.
 
 ```js
 array().of(yup.number());
-//or
+// or
 array(yup.number());
 ```
 
@@ -985,29 +995,29 @@ values to an empty array as well. Any non-empty, non-array value will be wrapped
 ```js
 array()
   .ensure()
-  .cast(null); // -> []
+  .cast(null); // => []
 array()
   .ensure()
-  .cast(1); // -> [1]
+  .cast(1); // => [1]
 array()
   .ensure()
-  .cast([1]); // -> [1]
+  .cast([1]); // => [1]
 ```
 
 #### `array.compact(rejector: (value) => boolean): Schema`
 
 Removes falsey values from the array. Providing a rejecter function lets you specify the rejection criteria yourself.
 
-```javascript
+```js
 array()
   .compact()
-  .cast(['', 1, 0, 4, false, null]); // => [1,4]
+  .cast(['', 1, 0, 4, false, null]); // => [1, 4]
 
 array()
   .compact(function(v) {
     return v == null;
   })
-  .cast(['', 1, 0, 4, false, null]); // => ['',1, 0, 4, false]
+  .cast(['', 1, 0, 4, false, null]); // => ['', 1, 0, 4, false]
 ```
 
 ### object
@@ -1015,7 +1025,7 @@ array()
 Define an object schema. Options passed into `isValid` are also passed to child schemas.
 Supports all the same methods as [`mixed`](#mixed).
 
-```javascript
+```js
 yup.object().shape({
   name: string().required(),
   age: number()
@@ -1033,7 +1043,7 @@ You can also pass a shape to the object constructor as a convenience.
 object().shape({
   num: number(),
 });
-//or
+// or
 object({
   num: number(),
 });
@@ -1049,7 +1059,7 @@ Define the keys of the object and the schemas for said keys.
 
 Note that you can chain `shape` method, which acts like object extends, for example:
 
-```javascript
+```js
 object({
   a: string(),
   b: number(),
@@ -1061,7 +1071,7 @@ object({
 
 would be exactly the same as:
 
-```javascript
+```js
 object({
   a: string(),
   b: string(),
@@ -1073,15 +1083,15 @@ object({
 
 Transforms the specified key to a new key. If `alias` is `true` then the old key will be left.
 
-```javascript
-var schema = object({
+```js
+let schema = object({
   myProp: mixed(),
   Other: mixed(),
 })
   .from('prop', 'myProp')
   .from('other', 'Other', true);
 
-inst.cast({ prop: 5, other: 6 }); // => { myProp: 5, other: 6, Other: 6 }
+schema.cast({ prop: 5, other: 6 }); // => { myProp: 5, other: 6, Other: 6 }
 ```
 
 #### `object.noUnknown(onlyKnownKeys: boolean = true, message?: string | function): Schema`
@@ -1102,13 +1112,13 @@ Transforms all object keys to CONSTANT_CASE.
 The simplest way to extend an existing type is just to cache a configured schema and use that through your application.
 
 ```js
-var yup = require('yup');
-var parseFormats = ['MMM dd, yyy'];
-var invalidDate = new Date('');
+let yup = require('yup');
+let parseFormats = ['MMM dd, yyy'];
+let invalidDate = new Date('');
 
 module.exports = yup.date().transform(function(value, originalValue) {
   if (this.isType(value)) return value;
-  //the default coercion transform failed so lets try it with Moment instead
+  // the default coercion transform failed so lets try it with Moment instead
   value = Moment(originalValue, parseFormats);
   return value.isValid() ? value.toDate() : invalidDate;
 });
@@ -1130,7 +1140,7 @@ You should keep in mind some basic guidelines when extending schemas
 **Adjust core Types**
 
 ```js
-var invalidDate = new Date('');
+let invalidDate = new Date('');
 
 function parseDateFromFormats(formats, parseStrict) {
   return this.transform(function(value, originalValue) {
@@ -1154,8 +1164,8 @@ utility or pattern that works with that pattern. The below demonstrates using th
 syntax since its less verbose, but you absolutely aren't required to use it.
 
 ```js
-var DateSchema = yup.date;
-var invalidDate = new Date(''); // our failed to coerce value
+let DateSchema = yup.date;
+let invalidDate = new Date(''); // our failed to coerce value
 
 class MomentDateSchemaType extends DateSchema {
   constructor() {
@@ -1185,7 +1195,7 @@ class MomentDateSchemaType extends DateSchema {
   }
 }
 
-var schema = new MomentDateSchemaType();
+let schema = new MomentDateSchemaType();
 
-schema.format('YYYY-MM-DD').cast('It is 2012-05-25'); // Fri May 25 2012 00:00:00 GMT-0400 (Eastern Daylight Time)
+schema.format('YYYY-MM-DD').cast('It is 2012-05-25'); // => Fri May 25 2012 00:00:00 GMT-0400 (Eastern Daylight Time)
 ```
diff --git a/package.json b/package.json
index 5f7f2f6..b2159e8 100644
--- a/package.json
+++ b/package.json
@@ -88,7 +88,7 @@
     "sinon-chai": "^3.2.0"
   },
   "dependencies": {
-    "@babel/runtime": "7.0.0",
+    "@babel/runtime": "^7.0.0",
     "fn-name": "~2.0.1",
     "lodash": "^4.17.10",
     "property-expr": "^1.5.0",
diff --git a/src/Condition.js b/src/Condition.js
index 9d03437..a5d9024 100644
--- a/src/Condition.js
+++ b/src/Condition.js
@@ -1,55 +1,64 @@
 import has from 'lodash/has';
 import isSchema from './util/isSchema';
 
-function callOrConcat(schema) {
-  if (typeof schema === 'function') return schema;
-
-  return base => base.concat(schema);
+function wrapCusomFn(fn) {
+  return function(...args) {
+    args.pop();
+    return fn.apply(this, args);
+  };
 }
 
-class Conditional {
-  constructor(refs, options) {
-    let { is, then, otherwise } = options;
+function makeFn(options) {
+  if (typeof options === 'function') return wrapCusomFn(options);
 
-    this.refs = [].concat(refs);
+  if (!has(options, 'is'))
+    throw new TypeError('`is:` is required for `when()` conditions');
 
-    then = callOrConcat(then);
-    otherwise = callOrConcat(otherwise);
+  if (!options.then && !options.otherwise)
+    throw new TypeError(
+      'either `then:` or `otherwise:` is required for `when()` conditions',
+    );
 
-    if (typeof options === 'function') this.fn = options;
-    else {
-      if (!has(options, 'is'))
-        throw new TypeError('`is:` is required for `when()` conditions');
+  let { is, then, otherwise } = options;
 
-      if (!options.then && !options.otherwise)
-        throw new TypeError(
-          'either `then:` or `otherwise:` is required for `when()` conditions',
-        );
+  let check;
+  if (typeof is === 'function') {
+    check = is;
+  } else {
+    check = (...values) => values.every(value => value === is);
+  }
 
-      let isFn =
-        typeof is === 'function'
-          ? is
-          : (...values) => values.every(value => value === is);
+  let fn = function(...args) {
+    let options = args.pop();
+    let schema = args.pop();
+    let branch = check(...args) ? then : otherwise;
 
-      this.fn = function(...values) {
-        let currentSchema = values.pop();
-        let option = isFn(...values) ? then : otherwise;
+    if (!branch) return undefined;
+    if (typeof branch === 'function') return branch(schema);
+    return schema.concat(branch.resolve(options));
+  };
 
-        return option(currentSchema);
-      };
-    }
+  return fn;
+}
+
+class Condition {
+  constructor(refs, options) {
+    this.refs = refs;
+    this.fn = makeFn(options);
   }
 
-  resolve(ctx, options) {
+  resolve(base, options) {
     let values = this.refs.map(ref => ref.getValue(options));
 
-    let schema = this.fn.apply(ctx, values.concat(ctx));
+    let schema = this.fn.apply(base, values.concat(base, options));
+
+    if (schema === undefined || schema === base) return base;
 
-    if (schema !== undefined && !isSchema(schema))
+    if (!isSchema(schema))
       throw new TypeError('conditions must return a schema object');
 
-    return schema || ctx;
+    return schema.resolve(options);
   }
 }
 
-export default Conditional;
+export default Condition;
diff --git a/src/Lazy.js b/src/Lazy.js
index d092667..0de633a 100644
--- a/src/Lazy.js
+++ b/src/Lazy.js
@@ -2,16 +2,17 @@ import isSchema from './util/isSchema';
 
 class Lazy {
   constructor(mapFn) {
-    this._resolve = (...args) => {
-      let schema = mapFn(...args);
+    this._resolve = (value, options) => {
+      let schema = mapFn(value, options);
+
       if (!isSchema(schema))
         throw new TypeError('lazy() functions must return a valid schema');
 
-      return schema;
+      return schema.resolve(options);
     };
   }
-  resolve({ value, ...rest }) {
-    return this._resolve(value, rest);
+  resolve(options) {
+    return this._resolve(options.value, options);
   }
   cast(value, options) {
     return this._resolve(value, options).cast(value, options);
diff --git a/src/array.js b/src/array.js
index e91084a..6d8c879 100644
--- a/src/array.js
+++ b/src/array.js
@@ -4,11 +4,9 @@ import isSchema from './util/isSchema';
 import makePath from './util/makePath';
 import printValue from './util/printValue';
 import MixedSchema from './mixed';
-import { mixed, array as locale } from './locale';
+import { array as locale } from './locale';
 import runValidations, { propagateErrors } from './util/runValidations';
 
-let hasLength = value => !isAbsent(value) && value.length > 0;
-
 export default ArraySchema;
 
 function ArraySchema(type) {
@@ -110,6 +108,10 @@ inherits(ArraySchema, MixedSchema, {
       });
   },
 
+  _isFilled(value) {
+    return value.length > 0;
+  },
+
   of(schema) {
     var next = this.clone();
 
@@ -125,16 +127,6 @@ inherits(ArraySchema, MixedSchema, {
     return next;
   },
 
-  required(message = mixed.required) {
-    var next = MixedSchema.prototype.required.call(this, message);
-
-    return next.test({
-      message,
-      name: 'required',
-      test: hasLength,
-    });
-  },
-
   min(min, message) {
     message = message || locale.min;
 
diff --git a/src/mixed.js b/src/mixed.js
index 27f781c..cd95774 100644
--- a/src/mixed.js
+++ b/src/mixed.js
@@ -13,8 +13,6 @@ import printValue from './util/printValue';
 import Ref from './Reference';
 import { getIn } from './util/reach';
 
-let notEmpty = value => !isAbsent(value);
-
 class RefSet {
   constructor() {
     this.list = new Set();
@@ -96,9 +94,10 @@ const proto = (SchemaType.prototype = {
   },
 
   withMutation(fn) {
+    let before = this._mutate;
     this._mutate = true;
     let result = fn(this);
-    this._mutate = false;
+    this._mutate = before;
     return result;
   },
 
@@ -122,8 +121,10 @@ const proto = (SchemaType.prototype = {
 
     // manually add the new tests to ensure
     // the deduping logic is consistent
-    schema.tests.forEach(fn => {
-      next = next.test(fn.OPTIONS);
+    next.withMutation(next => {
+      schema.tests.forEach(fn => {
+        next.test(fn.OPTIONS);
+      });
     });
 
     return next;
@@ -135,14 +136,22 @@ const proto = (SchemaType.prototype = {
   },
 
   resolve(options) {
-    if (this._conditions.length) {
-      return this._conditions.reduce(
+    let schema = this;
+
+    if (schema._conditions.length) {
+      let conditions = schema._conditions;
+
+      schema = schema.clone();
+      schema._conditions = [];
+      schema = conditions.reduce(
         (schema, condition) => condition.resolve(schema, options),
-        this,
+        schema,
       );
+
+      schema = schema.resolve(options);
     }
 
-    return this;
+    return schema;
   },
 
   cast(value, options = {}) {
@@ -296,14 +305,25 @@ const proto = (SchemaType.prototype = {
     return next;
   },
 
-  strict() {
+  strict(isStrict = true) {
     var next = this.clone();
-    next._options.strict = true;
+    next._options.strict = isStrict;
     return next;
   },
 
+  _isFilled(_) {
+    return true;
+  },
+
   required(message = locale.required) {
-    return this.test({ message, name: 'required', test: notEmpty });
+    return this.test({
+      message,
+      name: 'required',
+      exclusive: true,
+      test(value) {
+        return !isAbsent(value) && this.schema._isFilled(value);
+      },
+    });
   },
 
   notRequired() {
@@ -312,9 +332,9 @@ const proto = (SchemaType.prototype = {
     return next;
   },
 
-  nullable(value) {
+  nullable(isNullable = true) {
     var next = this.clone();
-    next._nullable = value === false ? false : true;
+    next._nullable = isNullable;
     return next;
   },
 
@@ -338,16 +358,22 @@ const proto = (SchemaType.prototype = {
    * the previous tests are removed and further tests of the same name will replace each other.
    */
   test(...args) {
-    let opts = args[0];
-    if (args.length > 1) {
-      let [name, message, test] = args;
-      if (test == null) {
-        test = message;
-        message = locale.default;
+    let opts;
+
+    if (args.length === 1) {
+      if (typeof args[0] === 'function') {
+        opts = { test: args[0] };
+      } else {
+        opts = args[0];
       }
-      opts = { name, test, message, exclusive: false };
+    } else if (args.length === 2) {
+      opts = { name: args[0], test: args[1] };
+    } else {
+      opts = { name: args[0], message: args[1], test: args[2] };
     }
 
+    if (opts.message === undefined) opts.message = locale.default;
+
     if (typeof opts.test !== 'function')
       throw new TypeError('`test` is a required parameters');
 
@@ -512,3 +538,4 @@ for (const method of ['validate', 'validateSync'])
 
 for (const alias of ['equals', 'is']) proto[alias] = proto.oneOf;
 for (const alias of ['not', 'nope']) proto[alias] = proto.notOneOf;
+proto.optional = proto.notRequired;
diff --git a/src/object.js b/src/object.js
index 417e83b..fdd5110 100644
--- a/src/object.js
+++ b/src/object.js
@@ -245,6 +245,10 @@ inherits(ObjectSchema, MixedSchema, {
     return next;
   },
 
+  unknown(allow = true, message = locale.noUnknown) {
+    return this.noUnknown(!allow, message);
+  },
+
   transformKeys(fn) {
     return this.transform(obj => obj && mapKeys(obj, (_, key) => fn(key)));
   },
diff --git a/src/string.js b/src/string.js
index f6d2677..4ca33ad 100644
--- a/src/string.js
+++ b/src/string.js
@@ -1,6 +1,6 @@
 import inherits from './util/inherits';
 import MixedSchema from './mixed';
-import { mixed, string as locale } from './locale';
+import { string as locale } from './locale';
 import isAbsent from './util/isAbsent';
 
 // eslint-disable-next-line
@@ -8,7 +8,6 @@ let rEmail = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\u
 // 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;
 
-let hasLength = value => isAbsent(value) || value.length > 0;
 let isTrimmed = value => isAbsent(value) || value === value.trim();
 
 export default function StringSchema() {
@@ -31,10 +30,8 @@ inherits(StringSchema, MixedSchema, {
     return typeof value === 'string';
   },
 
-  required(message = mixed.required) {
-    var next = MixedSchema.prototype.required.call(this, message);
-
-    return next.test({ message, name: 'required', test: hasLength });
+  _isFilled(value) {
+    return value.length > 0;
   },
 
   length(length, message = locale.length) {
diff --git a/src/util/reach.js b/src/util/reach.js
index d033d19..1f8fae7 100644
--- a/src/util/reach.js
+++ b/src/util/reach.js
@@ -13,7 +13,7 @@ export function getIn(schema, path, value, context) {
     return {
       parent,
       parentPath: path,
-      schema: schema.resolve({ context, parent, value }),
+      schema,
     };
 
   forEach(path, (_part, isBracket, isArray) => {
@@ -55,10 +55,6 @@ export function getIn(schema, path, value, context) {
     }
   });
 
-  if (schema) {
-    schema = schema.resolve({ context, parent, value });
-  }
-
   return { schema, parent, parentPath: lastPart };
 }
 


Test patch:
diff --git a/test/mixed.js b/test/mixed.js
index c012857..3fadcc5 100644
--- a/test/mixed.js
+++ b/test/mixed.js
@@ -7,6 +7,7 @@ import {
   ref,
   reach,
   bool,
+  lazy,
   ValidationError,
 } from '../src';
 
@@ -349,6 +350,14 @@ describe('Mixed Types ', () => {
     inst.tests[0].OPTIONS.message.should.equal('${path} is invalid');
   });
 
+  it('should fallback to default message', async () => {
+    let inst = mixed().test(() => false);
+
+    await inst
+      .validate('foo')
+      .should.be.rejectedWith(ValidationError, 'this is invalid');
+  });
+
   it('should allow non string messages', async () => {
     let message = { key: 'foo' };
     let inst = mixed().test('test', message, () => false);
@@ -521,6 +530,55 @@ describe('Mixed Types ', () => {
       });
   });
 
+  describe('withMutation', () => {
+    it('should pass the same instance to a provided function', () => {
+      let inst = mixed();
+      let func = sinon.spy();
+
+      inst.withMutation(func);
+
+      func.should.have.been.calledOnceWithExactly(inst);
+    });
+
+    it('should temporarily make mutable', () => {
+      let inst = mixed();
+
+      let update = () => {
+        inst.withMutation(inst => {
+          inst.test('a', () => true);
+        });
+      };
+
+      update.should.increase(() => inst.tests.length).by(1);
+    });
+
+    it('should return immutability', () => {
+      let inst = mixed();
+      inst.withMutation(() => {});
+
+      let update = () => {
+        inst.test('a', () => true);
+      };
+
+      update.should.not.increase(() => inst.tests.length);
+    });
+
+    it('should work with nesting', () => {
+      let inst = mixed();
+
+      let update = () => {
+        inst.withMutation(inst => {
+          inst.withMutation(inst => {
+            inst.test('a', () => true);
+          });
+          inst.test('b', () => true);
+        });
+      };
+
+      update.should.increase(() => inst.tests.length).by(2);
+    });
+  });
+
   describe('concat', () => {
     let next;
     let inst = object({
@@ -545,7 +603,7 @@ describe('Mixed Types ', () => {
     });
 
     it('should have the correct number of tests', () => {
-      reach(next, 'str').tests.length.should.equal(3); // presence, alt presence, and trim
+      reach(next, 'str').tests.length.should.equal(2);
     });
 
     it('should have the tests in the correct order', () => {
@@ -750,6 +808,28 @@ describe('Mixed Types ', () => {
     await inst.validate(-1).should.be.fulfilled();
   });
 
+  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)),
+      }),
+    });
+
+    await inst
+      .validate('pass', { context: { check: false } })
+      .should.be.fulfilled();
+
+    await inst
+      .validate('pass', { context: { check: 'hello' } })
+      .should.be.rejectedWith(ValidationError, /must be at least/);
+
+    await inst
+      .validate('passes', { context: { check: 'hello' } })
+      .should.be.fulfilled();
+  });
+
   it('should use label in error message', async function() {
     let label = 'Label';
     let inst = object({
diff --git a/test/object.js b/test/object.js
index 2069d51..705067f 100644
--- a/test/object.js
+++ b/test/object.js
@@ -448,8 +448,12 @@ describe('Object types', () => {
         }),
       });
 
-      reach(inst, 'nested').should.equal(inst);
-      reach(inst, 'x.y').should.equal(inst);
+      reach(inst, 'nested')
+        .resolve({})
+        .should.equal(inst);
+      reach(inst, 'x.y')
+        .resolve({})
+        .should.equal(inst);
     });
 
     it('should be passed the value', done => {
diff --git a/test/yup.js b/test/yup.js
index 9ec8d51..e877345 100644
--- a/test/yup.js
+++ b/test/yup.js
@@ -2,7 +2,7 @@ import reach, { getIn } from '../src/util/reach';
 import prependDeep from '../src/util/prependDeep';
 import { settled } from '../src/util/runValidations';
 
-import { object, array, string, lazy, number } from '../src';
+import { object, array, string, lazy, number, ValidationError } from '../src';
 
 describe('Yup', function() {
   it('cast should not assert on undefined', () => {
@@ -108,8 +108,8 @@ describe('Yup', function() {
     valid.should.equal(true);
   });
 
-  it('should REACH conditionally correctly', function() {
-    var num = number(),
+  it('should REACH conditionally correctly', async function() {
+    var num = number().oneOf([4]),
       inst = object().shape({
         num: number().max(4),
         nested: object().shape({
@@ -136,21 +136,42 @@ describe('Yup', function() {
       },
     };
 
-    reach(inst, 'nested.arr.num', value).should.equal(num);
-    reach(inst, 'nested.arr[].num', value).should.equal(num);
-
-    reach(inst, 'nested.arr.num', value, context).should.equal(num);
-    reach(inst, 'nested.arr[].num', value, context).should.equal(num);
-    reach(inst, 'nested.arr[0].num', value, context).should.equal(num);
-
-    // should fail b/c item[1] is used to resolve the schema
-    reach(inst, 'nested["arr"][1].num', value, context).should.not.equal(num);
-
-    return reach(inst, 'nested.arr[].num', value, context)
-      .isValid(5)
-      .then(valid => {
-        valid.should.equal(true);
-      });
+    let options = {};
+    options.parent = value.nested.arr[0];
+    options.value = options.parent.num;
+    reach(inst, 'nested.arr.num', value)
+      .resolve(options)
+      .should.equal(num);
+    reach(inst, 'nested.arr[].num', value)
+      .resolve(options)
+      .should.equal(num);
+
+    options.context = context;
+    reach(inst, 'nested.arr.num', value, context)
+      .resolve(options)
+      .should.equal(num);
+    reach(inst, 'nested.arr[].num', value, context)
+      .resolve(options)
+      .should.equal(num);
+    reach(inst, 'nested.arr[0].num', value, context)
+      .resolve(options)
+      .should.equal(num);
+
+    // // should fail b/c item[1] is used to resolve the schema
+    options.parent = value.nested.arr[1];
+    options.value = options.parent.num;
+    reach(inst, 'nested["arr"][1].num', value, context)
+      .resolve(options)
+      .should.not.equal(num);
+
+    let reached = reach(inst, 'nested.arr[].num', value, context);
+
+    await reached.validate(5, { context, parent: { foo: 4 } }).should.be
+      .fulfilled;
+
+    await reached
+      .validate(5, { context, parent: { foo: 5 } })
+      .should.be.rejectedWith(ValidationError, /one of the following/);
   });
 
   it('should reach through lazy', async () => {