diff --git a/__tests__/PropTypesDevelopmentReact15.js b/__tests__/PropTypesDevelopmentReact15.js index e98a7bf..c5c10e1 100644 --- a/__tests__/PropTypesDevelopmentReact15.js +++ b/__tests__/PropTypesDevelopmentReact15.js @@ -1045,6 +1045,113 @@ describe('PropTypesDevelopmentReact15', () => { }); }); + describe('Exact Types', () => { + it('should warn for non objects', () => { + typeCheckFail( + PropTypes.exact({}), + 'some string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected `object`.', + ); + typeCheckFail( + PropTypes.exact({}), + ['array'], + 'Invalid prop `testProp` of type `array` supplied to ' + + '`testComponent`, expected `object`.', + ); + }); + + it('should not warn for empty values', () => { + typeCheckPass(PropTypes.exact({}), undefined); + typeCheckPass(PropTypes.exact({}), null); + typeCheckPass(PropTypes.exact({}), {}); + }); + + it('should not warn for an empty object', () => { + typeCheckPass(PropTypes.exact({}).isRequired, {}); + }); + + it('should warn for non specified types', () => { + typeCheckFail( + PropTypes.exact({}), + {key: 1}, + 'Warning: Failed prop type: Invalid prop `testProp` key `key` supplied to `testComponent`.' + + '\nBad object: {' + + '\n \"key\": 1' + + '\n}' + + '\nValid keys: []' + ); + }); + + it('should not warn for valid types', () => { + typeCheckPass(PropTypes.exact({key: PropTypes.number}), {key: 1}); + }); + + it('should warn for required valid types', () => { + typeCheckFail( + PropTypes.exact({key: PropTypes.number.isRequired}), + {}, + 'The prop `testProp.key` is marked as required in `testComponent`, ' + + 'but its value is `undefined`.', + ); + }); + + it('should warn for the first required type', () => { + typeCheckFail( + PropTypes.exact({ + key: PropTypes.number.isRequired, + secondKey: PropTypes.number.isRequired, + }), + {}, + 'The prop `testProp.key` is marked as required in `testComponent`, ' + + 'but its value is `undefined`.', + ); + }); + + it('should warn for invalid key types', () => { + typeCheckFail( + PropTypes.exact({key: PropTypes.number}), + {key: 'abc'}, + 'Invalid prop `testProp.key` of type `string` supplied to `testComponent`, ' + + 'expected `number`.', + ); + }); + + it('should be implicitly optional and not warn without values', () => { + typeCheckPass( + PropTypes.exact(PropTypes.exact({key: PropTypes.number})), + null, + ); + typeCheckPass( + PropTypes.exact(PropTypes.exact({key: PropTypes.number})), + undefined, + ); + }); + + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.exact({key: PropTypes.number}).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.exact({}), 'some string'); + expectWarningInDevelopment(PropTypes.exact({foo: PropTypes.number}), { + foo: 42, + }); + expectWarningInDevelopment( + PropTypes.exact({key: PropTypes.number}).isRequired, + null, + ); + expectWarningInDevelopment( + PropTypes.exact({key: PropTypes.number}).isRequired, + undefined, + ); + expectWarningInDevelopment(PropTypes.element,
); + }); + }); + describe('Symbol Type', () => { it('should warn for non-symbol', () => { typeCheckFail( diff --git a/factoryWithThrowingShims.js b/factoryWithThrowingShims.js index 840f68e..18c8534 100644 --- a/factoryWithThrowingShims.js +++ b/factoryWithThrowingShims.js @@ -49,7 +49,8 @@ module.exports = function() { objectOf: getShim, oneOf: getShim, oneOfType: getShim, - shape: getShim + shape: getShim, + exact: getShim }; ReactPropTypes.checkPropTypes = emptyFunction; diff --git a/factoryWithTypeCheckers.js b/factoryWithTypeCheckers.js index 4da647e..8166312 100644 --- a/factoryWithTypeCheckers.js +++ b/factoryWithTypeCheckers.js @@ -12,6 +12,7 @@ var emptyFunction = require('fbjs/lib/emptyFunction'); var invariant = require('fbjs/lib/invariant'); var warning = require('fbjs/lib/warning'); +var assign = require('object-assign'); var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret'); var checkPropTypes = require('./checkPropTypes'); @@ -110,7 +111,8 @@ module.exports = function(isValidElement, throwOnDirectAccess) { objectOf: createObjectOfTypeChecker, oneOf: createEnumTypeChecker, oneOfType: createUnionTypeChecker, - shape: createShapeTypeChecker + shape: createShapeTypeChecker, + exact: createStrictShapeTypeChecker, }; /** @@ -379,6 +381,36 @@ module.exports = function(isValidElement, throwOnDirectAccess) { return createChainableTypeChecker(validate); } + function createStrictShapeTypeChecker(shapeTypes) { + function validate(props, propName, componentName, location, propFullName) { + var propValue = props[propName]; + var propType = getPropType(propValue); + if (propType !== 'object') { + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type `' + propType + '` ' + ('supplied to `' + componentName + '`, expected `object`.')); + } + // We need to check all keys in case some are required but missing from + // props. + var allKeys = assign({}, props[propName], shapeTypes); + for (var key in allKeys) { + var checker = shapeTypes[key]; + if (!checker) { + return new PropTypeError( + 'Invalid ' + location + ' `' + propFullName + '` key `' + key + '` supplied to `' + componentName + '`.' + + '\nBad object: ' + JSON.stringify(props[propName], null, ' ') + + '\nValid keys: ' + JSON.stringify(Object.keys(shapeTypes), null, ' ') + ); + } + var error = checker(propValue, key, componentName, location, propFullName + '.' + key, ReactPropTypesSecret); + if (error) { + return error; + } + } + return null; + } + + return createChainableTypeChecker(validate); + } + function isNode(propValue) { switch (typeof propValue) { case 'number': diff --git a/package.json b/package.json index 15eaa14..5229aeb 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "homepage": "https://facebook.github.io/react/", "dependencies": { "fbjs": "^0.8.9", - "loose-envify": "^1.3.1" + "loose-envify": "^1.3.1", + "object-assign": "^4.1.1" }, "scripts": { "test": "jest", diff --git a/yarn.lock b/yarn.lock index f979afa..fefa4e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2027,7 +2027,7 @@ oauth-sign@~0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@^4.1.0: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"