diff --git a/src/core/ReactPropTypes.js b/src/core/ReactPropTypes.js index 753d9d73b3..3bb52ffabc 100644 --- a/src/core/ReactPropTypes.js +++ b/src/core/ReactPropTypes.js @@ -81,6 +81,7 @@ var Props = { string: createPrimitiveTypeChecker('string'), oneOf: createEnumTypeChecker, + oneOfType: createUnionTypeChecker, instanceOf: createInstanceTypeChecker @@ -89,13 +90,19 @@ var Props = { var ANONYMOUS = '<>'; function createPrimitiveTypeChecker(expectedType) { - function validatePrimitiveType(propValue, propName, componentName, location) { + function validatePrimitiveType( + shouldThrow, propValue, propName, componentName, location + ) { var propType = typeof propValue; if (propType === 'object' && Array.isArray(propValue)) { propType = 'array'; } + var isValid = propType === expectedType; + if (!shouldThrow) { + return isValid; + } invariant( - propType === expectedType, + isValid, 'Invalid %s `%s` of type `%s` supplied to `%s`, expected `%s`.', ReactPropTypeLocationNames[location], propName, @@ -109,9 +116,15 @@ function createPrimitiveTypeChecker(expectedType) { function createEnumTypeChecker(expectedValues) { var expectedEnum = createObjectFrom(expectedValues); - function validateEnumType(propValue, propName, componentName, location) { + function validateEnumType( + shouldThrow, propValue, propName, componentName, location + ) { + var isValid = expectedEnum[propValue]; + if (!shouldThrow) { + return isValid; + } invariant( - expectedEnum[propValue], + isValid, 'Invalid %s `%s` supplied to `%s`, expected one of %s.', ReactPropTypeLocationNames[location], propName, @@ -123,9 +136,15 @@ function createEnumTypeChecker(expectedValues) { } function createInstanceTypeChecker(expectedClass) { - function validateInstanceType(propValue, propName, componentName, location) { + function validateInstanceType( + shouldThrow, propValue, propName, componentName, location + ) { + var isValid = propValue instanceof expectedClass; + if (!shouldThrow) { + return isValid; + } invariant( - propValue instanceof expectedClass, + isValid, 'Invalid %s `%s` supplied to `%s`, expected instance of `%s`.', ReactPropTypeLocationNames[location], propName, @@ -137,28 +156,64 @@ function createInstanceTypeChecker(expectedClass) { } function createChainableTypeChecker(validate) { - function createTypeChecker(isRequired) { - function checkType(props, propName, componentName, location) { - var propValue = props[propName]; - if (propValue != null) { - // Only validate if there is a value to check. - validate(propValue, propName, componentName || ANONYMOUS, location); - } else { - invariant( - !isRequired, - 'Required %s `%s` was not specified in `%s`.', - ReactPropTypeLocationNames[location], - propName, - componentName || ANONYMOUS - ); + function checkType( + isRequired, shouldThrow, props, propName, componentName, location + ) { + var propValue = props[propName]; + if (propValue != null) { + // Only validate if there is a value to check. + return validate( + shouldThrow, + propValue, + propName, + componentName || ANONYMOUS, + location + ); + } else { + var isValid = !isRequired; + if (!shouldThrow) { + return isValid; + } + invariant( + isValid, + 'Required %s `%s` was not specified in `%s`.', + ReactPropTypeLocationNames[location], + propName, + componentName || ANONYMOUS + ); + } + } + + var checker = checkType.bind(null, false, true); + checker.weak = checkType.bind(null, false, false); + checker.isRequired = checkType.bind(null, true, true); + checker.weak.isRequired = checkType.bind(null, true, false); + checker.isRequired.weak = checker.weak.isRequired; + + return checker; +} + +function createUnionTypeChecker(arrayOfValidators) { + return function(props, propName, componentName, location) { + var isValid = false; + for (var ii = 0; ii < arrayOfValidators.length; ii++) { + var validate = arrayOfValidators[ii]; + if (typeof validate.weak === 'function') { + validate = validate.weak; + } + if (validate(props, propName, componentName, location)) { + isValid = true; + break; } } - if (!isRequired) { - checkType.isRequired = createTypeChecker(true); - } - return checkType; - } - return createTypeChecker(false); + invariant( + isValid, + 'Invalid %s `%s` supplied to `%s`.', + ReactPropTypeLocationNames[location], + propName, + componentName || ANONYMOUS + ); + }; } module.exports = Props; diff --git a/src/core/__tests__/ReactPropTypes-test.js b/src/core/__tests__/ReactPropTypes-test.js index cdea401015..94e3c4b5f1 100644 --- a/src/core/__tests__/ReactPropTypes-test.js +++ b/src/core/__tests__/ReactPropTypes-test.js @@ -84,6 +84,12 @@ describe('Primitive Types', function() { '`testComponent`.' ); }); + + it("should have a weak version that returns true/false", function() { + expect(typeCheck(Props.string.weak, null)()).toEqual(true); + expect(typeCheck(Props.string.weak.isRequired, null)()).toEqual(false); + expect(typeCheck(Props.string.isRequired.weak, null)()).toEqual(false); + }); }); describe('Enum Types', function() { @@ -115,6 +121,13 @@ describe('Enum Types', function() { expect(typeCheck(Props.oneOf(['red', 'blue']), null)).not.toThrow(); expect(typeCheck(Props.oneOf(['red', 'blue']), undefined)).not.toThrow(); }); + + it("should have a weak version that returns true/false", function() { + var checker = Props.oneOf(['red', 'blue']); + expect(typeCheck(checker.weak, null)()).toEqual(true); + expect(typeCheck(checker.weak.isRequired, null)()).toEqual(false); + expect(typeCheck(checker.isRequired.weak, null)()).toEqual(false); + }); }); describe('Instance Types', function() { @@ -148,3 +161,48 @@ describe('Instance Types', function() { expect(typeCheck(Props.instanceOf(Person), new Engineer())).not.toThrow(); }); }); + +describe('Union Types', function() { + beforeEach(function() { + require('mock-modules').dumpCache(); + }); + + it('should throw if none of the types are valid', function() { + var checker = Props.oneOfType([ + Props.string, + Props.number + ]); + expect(typeCheck(checker, [])).toThrow( + 'Invariant Violation: Invalid prop `testProp` ' + + 'supplied to `testComponent`.' + ); + + checker = Props.oneOfType([ + Props.string.isRequired, + Props.number.isRequired + ]); + expect(typeCheck(checker, null)).toThrow( + 'Invariant Violation: Invalid prop `testProp` ' + + 'supplied to `testComponent`.' + ); + }); + + it('should not throw if one of the types are valid', function() { + var checker = Props.oneOfType([ + Props.string, + Props.number + ]); + expect(typeCheck(checker, null)).not.toThrow(); + expect(typeCheck(checker, 'foo')).not.toThrow(); + expect(typeCheck(checker, 123)).not.toThrow(); + + checker = Props.oneOfType([ + Props.string, + Props.number.isRequired + ]); + expect(typeCheck(checker, null)).not.toThrow(); + expect(typeCheck(checker, 'foo')).not.toThrow(); + expect(typeCheck(checker, 123)).not.toThrow(); + }); + +});