ReactPropTypes: add oneOfType to support union types

Summary:
e.g.

  propTypes: {
    foo: React.PropTypes.oneOfType([
      React.PropTypes.string,
      React.PropTypes.number
    ]);
  }

to do this, I exposed a `.weak` chainable validator that returns instead of throws, e.g.

  React.PropTypes.string.isRequired.weak
  React.PropTypes.string.weak

which returns true or false. Technically, `React.PropTypes.string.weak.isRequired` also exists, but `oneOfType` will never call it. Might be useful to people creating custom validators though.
This commit is contained in:
Marshall Roch
2013-11-27 12:10:50 -08:00
committed by Paul O’Shannessy
parent 351dcfed01
commit 697a9113c0
2 changed files with 139 additions and 26 deletions
+81 -26
View File
@@ -81,6 +81,7 @@ var Props = {
string: createPrimitiveTypeChecker('string'),
oneOf: createEnumTypeChecker,
oneOfType: createUnionTypeChecker,
instanceOf: createInstanceTypeChecker
@@ -89,13 +90,19 @@ var Props = {
var ANONYMOUS = '<<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;
+58
View File
@@ -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();
});
});