From 8265af568f0cdcd998d0ca8b28f6fe472105f7ac Mon Sep 17 00:00:00 2001 From: james kyle Date: Tue, 6 Jun 2017 10:30:53 -0700 Subject: [PATCH] Add exact type checker from react native (#41) --- __tests__/PropTypesDevelopmentReact15.js | 107 +++++++++++++++++++++++ factoryWithTypeCheckers.js | 33 ++++++- 2 files changed, 139 insertions(+), 1 deletion(-) 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/factoryWithTypeCheckers.js b/factoryWithTypeCheckers.js index 4da647e..3004dc9 100644 --- a/factoryWithTypeCheckers.js +++ b/factoryWithTypeCheckers.js @@ -110,7 +110,8 @@ module.exports = function(isValidElement, throwOnDirectAccess) { objectOf: createObjectOfTypeChecker, oneOf: createEnumTypeChecker, oneOfType: createUnionTypeChecker, - shape: createShapeTypeChecker + shape: createShapeTypeChecker, + exact: createStrictShapeTypeChecker, }; /** @@ -379,6 +380,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 = Object.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':