diff --git a/CHANGELOG.md b/CHANGELOG.md index 025d2f4..42cec08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 15.7.3 +* [New] Add `.tupleOf`, `.iterableOf`, `.mapOf`, and `.setOf` ([#289](https://github.com/facebook/prop-types/pull/289)) + ## 15.7.2 * [Fix] ensure nullish values in `oneOf` do not crash ([#256](https://github.com/facebook/prop-types/issues/256)) * [Fix] move `loose-envify` back to production deps, for browerify usage ([#203](https://github.com/facebook/prop-types/issues/203)) diff --git a/README.md b/README.md index fc631c5..e0f162f 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,18 @@ MyComponent.propTypes = { // An array of a certain type optionalArrayOf: PropTypes.arrayOf(PropTypes.number), + // A two item array of [string, number] + optionalTupleOf: PropTypes.tupleOf([PropTypes.string, PropTypes.number]), + + // An iterable of a certain type + optionalIterableOf: PropTypes.iterableOf(PropTypes.number), + + // An es6 Map containing certain types of keys and values + optionalMapOf: PropTypes.mapOf([PropTypes.string, PropTypes.number]), + + // An es6 Set containing certain types of values + optionalSetOf: PropTypes.setOf(PropTypes.number), + // An object with property values of a certain type optionalObjectOf: PropTypes.objectOf(PropTypes.number), diff --git a/__tests__/PropTypesDevelopmentReact15.js b/__tests__/PropTypesDevelopmentReact15.js index 42ebafb..5e8f2ea 100644 --- a/__tests__/PropTypesDevelopmentReact15.js +++ b/__tests__/PropTypesDevelopmentReact15.js @@ -538,6 +538,453 @@ describe('PropTypesDevelopmentReact15', () => { }); }); + describe('IterableOf Type', () => { + it('should fail for invalid argument', () => { + typeCheckFail( + PropTypes.iterableOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside iterableOf.', + ); + }); + + it('should support the iterableOf propTypes', () => { + typeCheckPass(PropTypes.iterableOf(PropTypes.number), [1, 2, 3]); + typeCheckPass(PropTypes.iterableOf(PropTypes.string), ['a', 'b', 'c']); + typeCheckPass(PropTypes.iterableOf(PropTypes.oneOf(['a', 'b'])), ['a', 'b']); + typeCheckPass(PropTypes.iterableOf(PropTypes.symbol), [Symbol(), Symbol()]); + }); + + it('should support iterableOf with complex types', () => { + typeCheckPass( + PropTypes.iterableOf(PropTypes.shape({ a: PropTypes.number.isRequired })), + [{ a: 1 }, { a: 2 }], + ); + + function Thing() { } + typeCheckPass(PropTypes.iterableOf(PropTypes.instanceOf(Thing)), [ + new Thing(), + new Thing(), + ]); + }); + + it('should warn with invalid items in the iterable', () => { + typeCheckFail( + PropTypes.iterableOf(PropTypes.number), + [1, 2, 'b'], + 'Invalid prop `testProp` of type `string` supplied to `testComponent`, expected `number`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + typeCheckFail( + PropTypes.iterableOf(PropTypes.instanceOf(Thing)), + [new Thing(), 'xyz'], + 'Invalid prop `testProp` of type `String` supplied to `testComponent`, expected instance of `' + + name + + '`.', + ); + }); + + it('should warn when passed something other than an iterable', () => { + typeCheckFail( + PropTypes.iterableOf(PropTypes.number), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to `testComponent`, expected an iterable.', + ); + typeCheckFail( + PropTypes.iterableOf(PropTypes.number), + 123, + 'Invalid prop `testProp` of type `number` supplied to `testComponent`, expected an iterable.', + ); + }); + + it('should not warn when passing an empty array', () => { + typeCheckPass(PropTypes.iterableOf(PropTypes.number), []); + }); + + it('should be implicitly optional and not warn without values', () => { + typeCheckPass(PropTypes.iterableOf(PropTypes.number), null); + typeCheckPass(PropTypes.iterableOf(PropTypes.number), undefined); + }); + + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.iterableOf(PropTypes.number).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.iterableOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectWarningInDevelopment(PropTypes.iterableOf(PropTypes.number), [ + 1, + 2, + 'b', + ]); + expectWarningInDevelopment(PropTypes.iterableOf(PropTypes.number), { + '0': 'maybe-array', + length: 1, + }); + expectWarningInDevelopment( + PropTypes.iterableOf(PropTypes.number).isRequired, + null, + ); + expectWarningInDevelopment( + PropTypes.iterableOf(PropTypes.number).isRequired, + undefined, + ); + }); + }); + + describe('TupleOf Type', () => { + it('should fail for invalid argument', () => { + typeCheckFail( + PropTypes.tupleOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside tupleOf.', + ); + }); + + it('should support the tupleOf propTypes', () => { + typeCheckPass(PropTypes.tupleOf([PropTypes.number, PropTypes.number, PropTypes.number]), [1, 2, 3]); + typeCheckPass(PropTypes.tupleOf([PropTypes.string, PropTypes.string]), ['a', 'b']); + typeCheckPass(PropTypes.tupleOf([PropTypes.oneOf(['a', 'b']), PropTypes.oneOf(['a', 'b'])]), ['a', 'b']); + typeCheckPass(PropTypes.tupleOf([PropTypes.symbol]), [Symbol()]); + }); + + it('should support tupleOf with complex types', () => { + typeCheckPass( + PropTypes.tupleOf([PropTypes.shape({ a: PropTypes.number.isRequired })]), + [{ a: 1 }], + ); + + function Thing() { } + typeCheckPass(PropTypes.tupleOf([PropTypes.instanceOf(Thing)]), [ + new Thing(), + ]); + }); + + it('should warn with invalid items in the array', () => { + typeCheckFail( + PropTypes.tupleOf([PropTypes.number, PropTypes.number]), + [1, 'b'], + 'Invalid prop `testProp[1]` of type `string` supplied to `testComponent`, expected `number`.', + ); + }); + + it('should warn with the wrong number of items in the array', () => { + typeCheckFail( + PropTypes.tupleOf([PropTypes.number]), + [1, 2], + 'Invalid prop `testProp` of length `2` supplied to `testComponent`, expected an array of length `1`.', + ); + + typeCheckFail( + PropTypes.tupleOf([PropTypes.number, PropTypes.number]), + [1], + 'Invalid prop `testProp` of length `1` supplied to `testComponent`, expected an array of length `2`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + typeCheckFail( + PropTypes.tupleOf([PropTypes.instanceOf(Thing), PropTypes.instanceOf(Thing)]), + [new Thing(), 'xyz'], + 'Invalid prop `testProp[1]` of type `String` supplied to `testComponent`, expected instance of `' + name + '`.', + ); + }); + + it('should warn when passed something other than an array', () => { + typeCheckFail( + PropTypes.tupleOf([PropTypes.number]), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected an array.', + ); + typeCheckFail( + PropTypes.tupleOf([PropTypes.number]), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected an array.', + ); + typeCheckFail( + PropTypes.tupleOf([PropTypes.number]), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected an array.', + ); + }); + + it('should be implicitly optional and not warn without values', () => { + typeCheckPass(PropTypes.tupleOf([PropTypes.number]), null); + typeCheckPass(PropTypes.tupleOf([PropTypes.number]), undefined); + }); + + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.tupleOf([PropTypes.number]).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.tupleOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectWarningInDevelopment(PropTypes.tupleOf([PropTypes.number]), [ + 1, + 2, + 'b', + ]); + expectWarningInDevelopment(PropTypes.tupleOf([PropTypes.number]), { + '0': 'maybe-array', + length: 1, + }); + expectWarningInDevelopment( + PropTypes.tupleOf([PropTypes.number]).isRequired, + null, + ); + expectWarningInDevelopment( + PropTypes.tupleOf([PropTypes.number]).isRequired, + undefined, + ); + }); + }); + + describe('MapOf Type', () => { + it('should fail if argument is not an array', () => { + typeCheckFail( + PropTypes.mapOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Correct syntax is `PropTypes.mapOf([PropTypes.keyType, PropTypes.valueType])`.', + ); + }); + + it('should fail for invalid keyMatcher or valueMatcher', () => { + typeCheckFail( + PropTypes.mapOf([{ foo: PropTypes.string }]), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside mapOf.', + ); + }); + + it('should support the mapOf propTypes', () => { + typeCheckPass(PropTypes.mapOf([PropTypes.string, PropTypes.number]), new Map([['1', 1], ['2', 2], ['3', 3]])); + typeCheckPass(PropTypes.mapOf([PropTypes.number, PropTypes.string]), new Map([[0, 'a'], [1, 'b'], [2, 'c']])); + typeCheckPass(PropTypes.mapOf([PropTypes.object, PropTypes.oneOf(['a', 'b'])]), new Map([[{}, 'a'], [{}, 'b']])); + typeCheckPass(PropTypes.mapOf([PropTypes.symbol, PropTypes.func]), new Map([[Symbol(), function () { }]])); + }); + + it('should support mapOf with complex types', () => { + typeCheckPass( + PropTypes.mapOf([PropTypes.string, PropTypes.shape({ a: PropTypes.number.isRequired })]), + new Map([['foo', { a: 1 }], ['bar', { a: 2 }]]), + ); + + function Thing() { } + typeCheckPass(PropTypes.mapOf([PropTypes.string, PropTypes.instanceOf(Thing)]), new Map([ + ['foo', new Thing()], + ['bar', new Thing()], + ])); + }); + + it('should warn with invalid entries in the map', () => { + typeCheckFail( + PropTypes.mapOf([PropTypes.string, PropTypes.number]), + new Map([['1', 1], ['2', 2], ['3', 'b']]), + 'Invalid prop `testProp[1]` of type `string` supplied to `testComponent`, expected `number`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + typeCheckFail( + PropTypes.mapOf([PropTypes.any, PropTypes.instanceOf(Thing)]), + new Map([[new Thing(), new Thing()], ['a', 'xyz']]), + 'Invalid prop `testProp[1]` of type `String` supplied to `testComponent`, expected instance of `' + name + '`.', + ); + }); + + it('should warn when passed something other than a map', () => { + typeCheckFail( + PropTypes.mapOf([PropTypes.number, PropTypes.number]), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected a Map.', + ); + typeCheckFail( + PropTypes.mapOf([PropTypes.number, PropTypes.number]), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected a Map.', + ); + typeCheckFail( + PropTypes.mapOf([PropTypes.number, PropTypes.number]), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected a Map.', + ); + }); + + it('should not warn when passing an empty map', () => { + typeCheckPass(PropTypes.mapOf([PropTypes.number, PropTypes.number]), new Map()); + }); + + it('should be implicitly optional and not warn without values', () => { + typeCheckPass(PropTypes.mapOf([PropTypes.number, PropTypes.number]), null); + typeCheckPass(PropTypes.mapOf([PropTypes.number, PropTypes.number]), undefined); + }); + + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.mapOf([PropTypes.number, PropTypes.number]).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.mapOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectWarningInDevelopment(PropTypes.mapOf([PropTypes.number, PropTypes.number]), [ + 1, + 2, + 'b', + ]); + expectWarningInDevelopment(PropTypes.mapOf([PropTypes.number, PropTypes.number]), { + '0': 'maybe-array', + length: 1, + }); + expectWarningInDevelopment( + PropTypes.mapOf([PropTypes.number, PropTypes.number]).isRequired, + null, + ); + expectWarningInDevelopment( + PropTypes.mapOf([PropTypes.number, PropTypes.number]).isRequired, + undefined, + ); + }); + }); + + describe('SetOf Type', () => { + it('should fail for invalid argument', () => { + typeCheckFail( + PropTypes.setOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside setOf.', + ); + }); + + it('should support the setOf propTypes', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), new Set([1, 2, 3])); + typeCheckPass(PropTypes.setOf(PropTypes.string), new Set(['a', 'b', 'c'])); + typeCheckPass(PropTypes.setOf(PropTypes.oneOf(['a', 'b'])), new Set(['a', 'b'])); + typeCheckPass(PropTypes.setOf(PropTypes.symbol), new Set([Symbol()])); + }); + + it('should support setOf with complex types', () => { + typeCheckPass( + PropTypes.setOf(PropTypes.shape({ a: PropTypes.number.isRequired })), + new Set([{ a: 1 }, { a: 2 }]), + ); + + function Thing() { } + typeCheckPass(PropTypes.setOf(PropTypes.instanceOf(Thing)), new Set([ + new Thing(), + new Thing(), + ])); + }); + + it('should warn with invalid values in the set', () => { + typeCheckFail( + PropTypes.setOf(PropTypes.number), + new Set([1, 2, '3']), + 'Invalid prop `testProp` of type `string` supplied to `testComponent`, expected `number`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + typeCheckFail( + PropTypes.setOf(PropTypes.instanceOf(Thing)), + new Set([new Thing(), 'xyz']), + 'Invalid prop `testProp` of type `String` supplied to `testComponent`, expected instance of `' + name + '`.' + ); + }); + + it('should warn when passed something other than a set', () => { + typeCheckFail( + PropTypes.setOf(PropTypes.number), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected a Set.', + ); + typeCheckFail( + PropTypes.setOf(PropTypes.number), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected a Set.', + ); + typeCheckFail( + PropTypes.setOf(PropTypes.number), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected a Set.', + ); + }); + + it('should not warn when passing an empty set', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), new Set()); + }); + + it('should be implicitly optional and not warn without values', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), null); + typeCheckPass(PropTypes.setOf(PropTypes.number), undefined); + }); + + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.setOf(PropTypes.number).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.setOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectWarningInDevelopment(PropTypes.setOf(PropTypes.number), [ + 1, + 2, + 'b', + ]); + expectWarningInDevelopment(PropTypes.setOf(PropTypes.number), { + '0': 'maybe-array', + length: 1, + }); + expectWarningInDevelopment( + PropTypes.setOf(PropTypes.number).isRequired, + null, + ); + expectWarningInDevelopment( + PropTypes.setOf(PropTypes.number).isRequired, + undefined, + ); + }); + }); + describe('Component Type', () => { it('should support components', () => { diff --git a/__tests__/PropTypesDevelopmentStandalone-test.js b/__tests__/PropTypesDevelopmentStandalone-test.js index 3c32fe1..7dc76ad 100644 --- a/__tests__/PropTypesDevelopmentStandalone-test.js +++ b/__tests__/PropTypesDevelopmentStandalone-test.js @@ -535,6 +535,453 @@ describe('PropTypesDevelopmentStandalone', () => { }); }); + describe('IterableOf Type', () => { + it('should fail for invalid argument', () => { + typeCheckFail( + PropTypes.iterableOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside iterableOf.', + ); + }); + + it('should support the iterableOf propTypes', () => { + typeCheckPass(PropTypes.iterableOf(PropTypes.number), [1, 2, 3]); + typeCheckPass(PropTypes.iterableOf(PropTypes.string), ['a', 'b', 'c']); + typeCheckPass(PropTypes.iterableOf(PropTypes.oneOf(['a', 'b'])), ['a', 'b']); + typeCheckPass(PropTypes.iterableOf(PropTypes.symbol), [Symbol(), Symbol()]); + }); + + it('should support iterableOf with complex types', () => { + typeCheckPass( + PropTypes.iterableOf(PropTypes.shape({ a: PropTypes.number.isRequired })), + [{ a: 1 }, { a: 2 }], + ); + + function Thing() { } + typeCheckPass(PropTypes.iterableOf(PropTypes.instanceOf(Thing)), [ + new Thing(), + new Thing(), + ]); + }); + + it('should warn with invalid items in the iterable', () => { + typeCheckFail( + PropTypes.iterableOf(PropTypes.number), + [1, 2, 'b'], + 'Invalid prop `testProp` of type `string` supplied to `testComponent`, expected `number`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + typeCheckFail( + PropTypes.iterableOf(PropTypes.instanceOf(Thing)), + [new Thing(), 'xyz'], + 'Invalid prop `testProp` of type `String` supplied to `testComponent`, expected instance of `' + + name + + '`.', + ); + }); + + it('should warn when passed something other than an iterable', () => { + typeCheckFail( + PropTypes.iterableOf(PropTypes.number), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to `testComponent`, expected an iterable.', + ); + typeCheckFail( + PropTypes.iterableOf(PropTypes.number), + 123, + 'Invalid prop `testProp` of type `number` supplied to `testComponent`, expected an iterable.', + ); + }); + + it('should not warn when passing an empty array', () => { + typeCheckPass(PropTypes.iterableOf(PropTypes.number), []); + }); + + it('should be implicitly optional and not warn without values', () => { + typeCheckPass(PropTypes.iterableOf(PropTypes.number), null); + typeCheckPass(PropTypes.iterableOf(PropTypes.number), undefined); + }); + + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.iterableOf(PropTypes.number).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectThrowsInDevelopment(PropTypes.iterableOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectThrowsInDevelopment(PropTypes.iterableOf(PropTypes.number), [ + 1, + 2, + 'b', + ]); + expectThrowsInDevelopment(PropTypes.iterableOf(PropTypes.number), { + '0': 'maybe-array', + length: 1, + }); + expectThrowsInDevelopment( + PropTypes.iterableOf(PropTypes.number).isRequired, + null, + ); + expectThrowsInDevelopment( + PropTypes.iterableOf(PropTypes.number).isRequired, + undefined, + ); + }); + }); + + describe('TupleOf Type', () => { + it('should fail for invalid argument', () => { + typeCheckFail( + PropTypes.tupleOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside tupleOf.', + ); + }); + + it('should support the tupleOf propTypes', () => { + typeCheckPass(PropTypes.tupleOf([PropTypes.number, PropTypes.number, PropTypes.number]), [1, 2, 3]); + typeCheckPass(PropTypes.tupleOf([PropTypes.string, PropTypes.string]), ['a', 'b']); + typeCheckPass(PropTypes.tupleOf([PropTypes.oneOf(['a', 'b']), PropTypes.oneOf(['a', 'b'])]), ['a', 'b']); + typeCheckPass(PropTypes.tupleOf([PropTypes.symbol]), [Symbol()]); + }); + + it('should support tupleOf with complex types', () => { + typeCheckPass( + PropTypes.tupleOf([PropTypes.shape({ a: PropTypes.number.isRequired })]), + [{ a: 1 }], + ); + + function Thing() { } + typeCheckPass(PropTypes.tupleOf([PropTypes.instanceOf(Thing)]), [ + new Thing(), + ]); + }); + + it('should warn with invalid items in the array', () => { + typeCheckFail( + PropTypes.tupleOf([PropTypes.number, PropTypes.number]), + [1, 'b'], + 'Invalid prop `testProp[1]` of type `string` supplied to `testComponent`, expected `number`.', + ); + }); + + it('should warn with the wrong number of items in the array', () => { + typeCheckFail( + PropTypes.tupleOf([PropTypes.number]), + [1, 2], + 'Invalid prop `testProp` of length `2` supplied to `testComponent`, expected an array of length `1`.', + ); + + typeCheckFail( + PropTypes.tupleOf([PropTypes.number, PropTypes.number]), + [1], + 'Invalid prop `testProp` of length `1` supplied to `testComponent`, expected an array of length `2`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + typeCheckFail( + PropTypes.tupleOf([PropTypes.instanceOf(Thing), PropTypes.instanceOf(Thing)]), + [new Thing(), 'xyz'], + 'Invalid prop `testProp[1]` of type `String` supplied to `testComponent`, expected instance of `' + name + '`.', + ); + }); + + it('should warn when passed something other than an array', () => { + typeCheckFail( + PropTypes.tupleOf([PropTypes.number]), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected an array.', + ); + typeCheckFail( + PropTypes.tupleOf([PropTypes.number]), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected an array.', + ); + typeCheckFail( + PropTypes.tupleOf([PropTypes.number]), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected an array.', + ); + }); + + it('should be implicitly optional and not warn without values', () => { + typeCheckPass(PropTypes.tupleOf([PropTypes.number]), null); + typeCheckPass(PropTypes.tupleOf([PropTypes.number]), undefined); + }); + + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.tupleOf([PropTypes.number]).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectThrowsInDevelopment(PropTypes.tupleOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectThrowsInDevelopment(PropTypes.tupleOf([PropTypes.number]), [ + 1, + 2, + 'b', + ]); + expectThrowsInDevelopment(PropTypes.tupleOf([PropTypes.number]), { + '0': 'maybe-array', + length: 1, + }); + expectThrowsInDevelopment( + PropTypes.tupleOf([PropTypes.number]).isRequired, + null, + ); + expectThrowsInDevelopment( + PropTypes.tupleOf([PropTypes.number]).isRequired, + undefined, + ); + }); + }); + + describe('MapOf Type', () => { + it('should fail if argument is not an array', () => { + typeCheckFail( + PropTypes.mapOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Correct syntax is `PropTypes.mapOf([PropTypes.keyType, PropTypes.valueType])`.', + ); + }); + + it('should fail for invalid keyMatcher or valueMatcher', () => { + typeCheckFail( + PropTypes.mapOf([{ foo: PropTypes.string }]), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside mapOf.', + ); + }); + + it('should support the mapOf propTypes', () => { + typeCheckPass(PropTypes.mapOf([PropTypes.string, PropTypes.number]), new Map([['1', 1], ['2', 2], ['3', 3]])); + typeCheckPass(PropTypes.mapOf([PropTypes.number, PropTypes.string]), new Map([[0, 'a'], [1, 'b'], [2, 'c']])); + typeCheckPass(PropTypes.mapOf([PropTypes.object, PropTypes.oneOf(['a', 'b'])]), new Map([[{}, 'a'], [{}, 'b']])); + typeCheckPass(PropTypes.mapOf([PropTypes.symbol, PropTypes.func]), new Map([[Symbol(), function(){}]])); + }); + + it('should support mapOf with complex types', () => { + typeCheckPass( + PropTypes.mapOf([PropTypes.string, PropTypes.shape({ a: PropTypes.number.isRequired })]), + new Map([['foo', { a: 1 }], ['bar', { a: 2 }]]), + ); + + function Thing() { } + typeCheckPass(PropTypes.mapOf([PropTypes.string, PropTypes.instanceOf(Thing)]), new Map([ + ['foo', new Thing()], + ['bar', new Thing()], + ])); + }); + + it('should fail with invalid entries in the map', () => { + typeCheckFail( + PropTypes.mapOf([PropTypes.string, PropTypes.number]), + new Map([['1', 1], ['2', 2], ['3', 'b']]), + 'Invalid prop `testProp[1]` of type `string` supplied to `testComponent`, expected `number`.', + ); + }); + + it('should fail with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + typeCheckFail( + PropTypes.mapOf([PropTypes.any, PropTypes.instanceOf(Thing)]), + new Map([[new Thing(), new Thing()], ['a', 'xyz']]), + 'Invalid prop `testProp[1]` of type `String` supplied to `testComponent`, expected instance of `' + name + '`.', + ); + }); + + it('should fail when passed something other than a map', () => { + typeCheckFail( + PropTypes.mapOf([PropTypes.number, PropTypes.number]), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected a Map.', + ); + typeCheckFail( + PropTypes.mapOf([PropTypes.number, PropTypes.number]), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected a Map.', + ); + typeCheckFail( + PropTypes.mapOf([PropTypes.number, PropTypes.number]), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected a Map.', + ); + }); + + it('should not fail when passing an empty map', () => { + typeCheckPass(PropTypes.mapOf([PropTypes.number, PropTypes.number]), new Map()); + }); + + it('should be implicitly optional and not fail without values', () => { + typeCheckPass(PropTypes.mapOf([PropTypes.number, PropTypes.number]), null); + typeCheckPass(PropTypes.mapOf([PropTypes.number, PropTypes.number]), undefined); + }); + + it('should fail for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.mapOf([PropTypes.number, PropTypes.number]).isRequired, + ); + }); + + it('should fail if called manually in development', () => { + spyOn(console, 'error'); + expectThrowsInDevelopment(PropTypes.mapOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectThrowsInDevelopment(PropTypes.mapOf([PropTypes.number, PropTypes.number]), [ + 1, + 2, + 'b', + ]); + expectThrowsInDevelopment(PropTypes.mapOf([PropTypes.number, PropTypes.number]), { + '0': 'maybe-array', + length: 1, + }); + expectThrowsInDevelopment( + PropTypes.mapOf([PropTypes.number, PropTypes.number]).isRequired, + null, + ); + expectThrowsInDevelopment( + PropTypes.mapOf([PropTypes.number, PropTypes.number]).isRequired, + undefined, + ); + }); + }); + + describe('SetOf Type', () => { + it('should fail for invalid argument', () => { + typeCheckFail( + PropTypes.setOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside setOf.', + ); + }); + + it('should support the setOf propTypes', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), new Set([1, 2, 3])); + typeCheckPass(PropTypes.setOf(PropTypes.string), new Set(['a', 'b', 'c'])); + typeCheckPass(PropTypes.setOf(PropTypes.oneOf(['a', 'b'])), new Set(['a', 'b'])); + typeCheckPass(PropTypes.setOf(PropTypes.symbol), new Set([Symbol()])); + }); + + it('should support setOf with complex types', () => { + typeCheckPass( + PropTypes.setOf(PropTypes.shape({ a: PropTypes.number.isRequired })), + new Set([{ a: 1 }, { a: 2 }]), + ); + + function Thing() { } + typeCheckPass(PropTypes.setOf(PropTypes.instanceOf(Thing)), new Set([ + new Thing(), + new Thing(), + ])); + }); + + it('should fail with invalid values in the set', () => { + typeCheckFail( + PropTypes.setOf(PropTypes.number), + new Set([1, 2, '3']), + 'Invalid prop `testProp` of type `string` supplied to `testComponent`, expected `number`.', + ); + }); + + it('should fail with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + typeCheckFail( + PropTypes.setOf(PropTypes.instanceOf(Thing)), + new Set([new Thing(), 'xyz']), + 'Invalid prop `testProp` of type `String` supplied to `testComponent`, expected instance of `' + name + '`.', + ); + }); + + it('should fail when passed something other than a set', () => { + typeCheckFail( + PropTypes.setOf(PropTypes.number), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected a Set.', + ); + typeCheckFail( + PropTypes.setOf(PropTypes.number), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected a Set.', + ); + typeCheckFail( + PropTypes.setOf(PropTypes.number), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected a Set.', + ); + }); + + it('should not fail when passing an empty set', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), new Set()); + }); + + it('should be implicitly optional and not fail without values', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), null); + typeCheckPass(PropTypes.setOf(PropTypes.number), undefined); + }); + + it('should fail for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.setOf(PropTypes.number).isRequired, + ); + }); + + it('should fail if called manually in development', () => { + spyOn(console, 'error'); + expectThrowsInDevelopment(PropTypes.setOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectThrowsInDevelopment(PropTypes.setOf(PropTypes.number), [ + 1, + 2, + 'b', + ]); + expectThrowsInDevelopment(PropTypes.setOf(PropTypes.number), { + '0': 'maybe-array', + length: 1, + }); + expectThrowsInDevelopment( + PropTypes.setOf(PropTypes.number).isRequired, + null, + ); + expectThrowsInDevelopment( + PropTypes.setOf(PropTypes.number).isRequired, + undefined, + ); + }); + }); + describe('Component Type', () => { it('should support components', () => { diff --git a/__tests__/PropTypesProductionReact15-test.js b/__tests__/PropTypesProductionReact15-test.js index 66de1ad..3a15ca4 100644 --- a/__tests__/PropTypesProductionReact15-test.js +++ b/__tests__/PropTypesProductionReact15-test.js @@ -314,6 +314,471 @@ describe('PropTypesProductionReact15', () => { }); }); + describe('IterableOf Type', () => { + it('should fail for invalid argument', () => { + expectNoop( + PropTypes.iterableOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside iterableOf.', + ); + }); + + it('should support the iterableOf propTypes', () => { + expectNoop(PropTypes.iterableOf(PropTypes.number), [1, 2, 3]); + expectNoop(PropTypes.iterableOf(PropTypes.string), ['a', 'b', 'c']); + expectNoop(PropTypes.iterableOf(PropTypes.oneOf(['a', 'b'])), ['a', 'b']); + expectNoop(PropTypes.iterableOf(PropTypes.symbol), [Symbol(), Symbol()]); + }); + + it('should support iterableOf with complex types', () => { + expectNoop( + PropTypes.iterableOf(PropTypes.shape({ a: PropTypes.number.isRequired })), + [{ a: 1 }, { a: 2 }], + ); + + function Thing() { } + expectNoop(PropTypes.iterableOf(PropTypes.instanceOf(Thing)), [ + new Thing(), + new Thing(), + ]); + }); + + it('should warn with invalid items in the array', () => { + expectNoop( + PropTypes.iterableOf(PropTypes.number), + [1, 2, 'b'], + 'Invalid prop `testProp[2]` of type `string` supplied to ' + + '`testComponent`, expected `number`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + expectNoop( + PropTypes.iterableOf(PropTypes.instanceOf(Thing)), + [new Thing(), 'xyz'], + 'Invalid prop `testProp[1]` of type `String` supplied to ' + + '`testComponent`, expected instance of `' + + name + + '`.', + ); + }); + + it('should warn when passed something other than an iterable', () => { + expectNoop( + PropTypes.iterableOf(PropTypes.number), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected an iterable.', + ); + expectNoop( + PropTypes.iterableOf(PropTypes.number), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected an iterable.', + ); + expectNoop( + PropTypes.iterableOf(PropTypes.number), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected an iterable.', + ); + }); + + it('should not warn when passing an empty array', () => { + expectNoop(PropTypes.iterableOf(PropTypes.number), []); + }); + + it('should be implicitly optional and not warn without values', () => { + expectNoop(PropTypes.iterableOf(PropTypes.number), null); + expectNoop(PropTypes.iterableOf(PropTypes.number), undefined); + }); + + it('should warn for missing required values', () => { + expectNoop( + PropTypes.iterableOf(PropTypes.number).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectNoop(PropTypes.iterableOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectNoop(PropTypes.iterableOf(PropTypes.number), [ + 1, + 2, + 'b', + ]); + expectNoop(PropTypes.iterableOf(PropTypes.number), { + '0': 'maybe-array', + length: 1, + }); + expectNoop( + PropTypes.iterableOf(PropTypes.number).isRequired, + null, + ); + expectNoop( + PropTypes.iterableOf(PropTypes.number).isRequired, + undefined, + ); + }); + }); + + describe('TupleOf Type', () => { + it('should fail for invalid argument', () => { + expectNoop( + PropTypes.tupleOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside tupleOf.', + ); + }); + + it('should support the tupleOf propTypes', () => { + expectNoop(PropTypes.tupleOf([PropTypes.number, PropTypes.number, PropTypes.number]), [1, 2, 3]); + expectNoop(PropTypes.tupleOf([PropTypes.string, PropTypes.string]), ['a', 'b']); + expectNoop(PropTypes.tupleOf([PropTypes.oneOf(['a', 'b']), PropTypes.oneOf(['a', 'b'])]), ['a', 'b']); + expectNoop(PropTypes.tupleOf([PropTypes.symbol]), [Symbol()]); + }); + + it('should support tupleOf with complex types', () => { + expectNoop( + PropTypes.tupleOf([PropTypes.shape({ a: PropTypes.number.isRequired })]), + [{ a: 1 }], + ); + + function Thing() { } + expectNoop(PropTypes.tupleOf([PropTypes.instanceOf(Thing)]), [ + new Thing(), + ]); + }); + + it('should warn with invalid items in the array', () => { + expectNoop( + PropTypes.tupleOf([PropTypes.number, PropTypes.number]), + [1, 'b'], + 'Invalid prop `testProp[1]` of type `string` supplied to `testComponent`, expected `number`.', + ); + }); + + it('should warn with the wrong number of items in the array', () => { + expectNoop( + PropTypes.tupleOf([PropTypes.number]), + [1, 2], + 'Invalid prop `testProp` of length `2` supplied to `testComponent`, expected an array of length `1`.', + ); + + expectNoop( + PropTypes.tupleOf([PropTypes.number, PropTypes.number]), + [1], + 'Invalid prop `testProp` of length `1` supplied to `testComponent`, expected an array of length `2`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + expectNoop( + PropTypes.tupleOf([PropTypes.instanceOf(Thing), PropTypes.instanceOf(Thing)]), + [new Thing(), 'xyz'], + 'Invalid prop `testProp[1]` of type `String` supplied to `testComponent`, expected instance of `' + name + '`.', + ); + }); + + it('should warn when passed something other than an array', () => { + expectNoop( + PropTypes.tupleOf([PropTypes.number]), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected an array.', + ); + expectNoop( + PropTypes.tupleOf([PropTypes.number]), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected an array.', + ); + expectNoop( + PropTypes.tupleOf([PropTypes.number]), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected an array.', + ); + }); + + it('should be implicitly optional and not warn without values', () => { + expectNoop(PropTypes.tupleOf([PropTypes.number]), null); + expectNoop(PropTypes.tupleOf([PropTypes.number]), undefined); + }); + + it('should warn for missing required values', () => { + expectNoop( + PropTypes.tupleOf([PropTypes.number]).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectNoop(PropTypes.tupleOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectNoop(PropTypes.tupleOf([PropTypes.number]), [ + 1, + 2, + 'b', + ]); + expectNoop(PropTypes.tupleOf([PropTypes.number]), { + '0': 'maybe-array', + length: 1, + }); + expectNoop( + PropTypes.tupleOf([PropTypes.number]).isRequired, + null, + ); + expectNoop( + PropTypes.tupleOf([PropTypes.number]).isRequired, + undefined, + ); + }); + }); + + describe('MapOf Type', () => { + it('should fail if argument is not an array', () => { + expectNoop( + PropTypes.mapOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Correct syntax is `PropTypes.mapOf([PropTypes.keyType, PropTypes.valueType])`.', + ); + }); + + it('should fail for invalid keyMatcher or valueMatcher', () => { + expectNoop( + PropTypes.mapOf([{ foo: PropTypes.string }]), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside mapOf.', + ); + }); + + it('should support the mapOf propTypes', () => { + expectNoop(PropTypes.mapOf([PropTypes.string, PropTypes.number]), new Map([['1', 1], ['2', 2], ['3', 3]])); + expectNoop(PropTypes.mapOf([PropTypes.number, PropTypes.string]), new Map([[0, 'a'], [1, 'b'], [2, 'c']])); + expectNoop(PropTypes.mapOf([PropTypes.object, PropTypes.oneOf(['a', 'b'])]), new Map([[{}, 'a'], [{}, 'b']])); + expectNoop(PropTypes.mapOf([PropTypes.symbol, PropTypes.func]), new Map([[Symbol(), function () { }]])); + }); + + it('should support mapOf with complex types', () => { + expectNoop( + PropTypes.mapOf([PropTypes.string, PropTypes.shape({ a: PropTypes.number.isRequired })]), + new Map([['foo', { a: 1 }], ['bar', { a: 2 }]]), + ); + + function Thing() { } + expectNoop(PropTypes.mapOf([PropTypes.string, PropTypes.instanceOf(Thing)]), new Map([ + ['foo', new Thing()], + ['bar', new Thing()], + ])); + }); + + it('should warn with invalid entries in the map', () => { + expectNoop( + PropTypes.mapOf([PropTypes.string, PropTypes.number]), + new Map([['1', 1], ['2', 2], ['3', 'b']]), + 'Invalid prop `testProp.get("3")` of type `string` supplied to ' + + '`testComponent`, expected `number`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + expectNoop( + PropTypes.mapOf([PropTypes.any, PropTypes.instanceOf(Thing)]), + new Map([[new Thing(), new Thing()], ['a', 'xyz']]), + 'Invalid prop `testProp.get("a")` of type `String` supplied to ' + + '`testComponent`, expected instance of `' + + name + + '`.', + ); + }); + + it('should warn when passed something other than a map', () => { + expectNoop( + PropTypes.mapOf([PropTypes.number, PropTypes.number]), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected a Map.', + ); + expectNoop( + PropTypes.mapOf([PropTypes.number, PropTypes.number]), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected a Map.', + ); + expectNoop( + PropTypes.mapOf([PropTypes.number, PropTypes.number]), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected a Map.', + ); + }); + + it('should not warn when passing an empty map', () => { + expectNoop(PropTypes.mapOf([PropTypes.number, PropTypes.number]), new Map()); + }); + + it('should be implicitly optional and not warn without values', () => { + expectNoop(PropTypes.mapOf([PropTypes.number, PropTypes.number]), null); + expectNoop(PropTypes.mapOf([PropTypes.number, PropTypes.number]), undefined); + }); + + it('should warn for missing required values', () => { + expectNoop( + PropTypes.mapOf([PropTypes.number, PropTypes.number]).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectNoop(PropTypes.mapOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectNoop(PropTypes.mapOf([PropTypes.number, PropTypes.number]), [ + 1, + 2, + 'b', + ]); + expectNoop(PropTypes.mapOf([PropTypes.number, PropTypes.number]), { + '0': 'maybe-array', + length: 1, + }); + expectNoop( + PropTypes.mapOf([PropTypes.number, PropTypes.number]).isRequired, + null, + ); + expectNoop( + PropTypes.mapOf([PropTypes.number, PropTypes.number]).isRequired, + undefined, + ); + }); + }); + + describe('SetOf Type', () => { + it('should fail for invalid argument', () => { + expectNoop( + PropTypes.setOf({ foo: PropTypes.string }), + { foo: 'bar' }, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside setOf.', + ); + }); + + it('should support the setOf propTypes', () => { + expectNoop(PropTypes.setOf(PropTypes.number), new Set([1, 2, 3])); + expectNoop(PropTypes.setOf(PropTypes.string), new Set(['a', 'b', 'c'])); + expectNoop(PropTypes.setOf(PropTypes.oneOf(['a', 'b'])), new Set(['a', 'b'])); + expectNoop(PropTypes.setOf(PropTypes.symbol), new Set([Symbol()])); + }); + + it('should support setOf with complex types', () => { + expectNoop( + PropTypes.setOf(PropTypes.shape({ a: PropTypes.number.isRequired })), + new Set([{ a: 1 }, { a: 2 }]), + ); + + function Thing() { } + expectNoop(PropTypes.setOf(PropTypes.instanceOf(Thing)), new Set([ + new Thing(), + new Thing(), + ])); + }); + + it('should warn with invalid values in the set', () => { + expectNoop( + PropTypes.setOf(PropTypes.number), + new Set([1, 2, '3']), + 'Invalid prop `testProp.has("3")` of type `string` supplied to ' + + '`testComponent`, expected `number`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() { } + const name = Thing.name || '<>'; + + expectNoop( + PropTypes.setOf(PropTypes.instanceOf(Thing)), + new Set([new Thing(), 'xyz']), + 'Invalid prop `testProp.has("xyz")` of type `String` supplied to ' + + '`testComponent`, expected instance of `' + + name + + '`.', + ); + }); + + it('should warn when passed something other than a set', () => { + expectNoop( + PropTypes.setOf(PropTypes.number), + { '0': 'maybe-array', length: 1 }, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected a Set.', + ); + expectNoop( + PropTypes.setOf(PropTypes.number), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected a Set.', + ); + expectNoop( + PropTypes.setOf(PropTypes.number), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected a Set.', + ); + }); + + it('should not warn when passing an empty set', () => { + expectNoop(PropTypes.setOf(PropTypes.number), new Set()); + }); + + it('should be implicitly optional and not warn without values', () => { + expectNoop(PropTypes.setOf(PropTypes.number), null); + expectNoop(PropTypes.setOf(PropTypes.number), undefined); + }); + + it('should warn for missing required values', () => { + expectNoop( + PropTypes.setOf(PropTypes.number).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectNoop(PropTypes.setOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectNoop(PropTypes.setOf(PropTypes.number), [ + 1, + 2, + 'b', + ]); + expectNoop(PropTypes.setOf(PropTypes.number), { + '0': 'maybe-array', + length: 1, + }); + expectNoop( + PropTypes.setOf(PropTypes.number).isRequired, + null, + ); + expectNoop( + PropTypes.setOf(PropTypes.number).isRequired, + undefined, + ); + }); + }); + describe('Component Type', () => { it('should support components', () => { diff --git a/__tests__/PropTypesProductionStandalone-test.js b/__tests__/PropTypesProductionStandalone-test.js index 1e2ca0b..4734ecc 100644 --- a/__tests__/PropTypesProductionStandalone-test.js +++ b/__tests__/PropTypesProductionStandalone-test.js @@ -136,6 +136,106 @@ describe('PropTypesProductionStandalone', function() { }); }); + describe('IterableOf Type', function () { + it('should be a no-op', function () { + expectThrowsInProduction(PropTypes.iterableOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectThrowsInProduction(PropTypes.iterableOf(PropTypes.number), [ + 1, + 2, + 'b', + ]); + expectThrowsInProduction(PropTypes.iterableOf(PropTypes.number), { + '0': 'maybe-array', + length: 1, + }); + expectThrowsInProduction( + PropTypes.iterableOf(PropTypes.number).isRequired, + null, + ); + expectThrowsInProduction( + PropTypes.iterableOf(PropTypes.number).isRequired, + undefined, + ); + }); + }); + + describe('TupleOf Type', function () { + it('should be a no-op', function () { + expectThrowsInProduction(PropTypes.tupleOf({ foo: PropTypes.string }), { + foo: 'bar', + }); + expectThrowsInProduction(PropTypes.tupleOf([PropTypes.number]), [ + 1, + 2, + 'b', + ]); + expectThrowsInProduction(PropTypes.tupleOf([PropTypes.number]), { + '0': 'maybe-array', + length: 1, + }); + expectThrowsInProduction( + PropTypes.tupleOf([PropTypes.number]).isRequired, + null, + ); + expectThrowsInProduction( + PropTypes.tupleOf([PropTypes.number]).isRequired, + undefined, + ); + }); + }); + + describe('MapOf Type', function () { + it('should be a no-op', function () { + expectThrowsInProduction(PropTypes.mapOf([]), { + foo: 'bar', + }); + expectThrowsInProduction(PropTypes.mapOf([PropTypes.string, PropTypes.number]), [ + 1, + 2, + 'b', + ]); + expectThrowsInProduction(PropTypes.mapOf([PropTypes.string, PropTypes.number]), { + '0': 'maybe-array', + length: 1, + }); + expectThrowsInProduction( + PropTypes.mapOf([PropTypes.string, PropTypes.number]).isRequired, + null, + ); + expectThrowsInProduction( + PropTypes.mapOf([PropTypes.string, PropTypes.number]).isRequired, + undefined, + ); + }); + }); + + describe('SetOf Type', function () { + it('should be a no-op', function () { + expectThrowsInProduction(PropTypes.setOf({}), { + foo: 'bar', + }); + expectThrowsInProduction(PropTypes.setOf(PropTypes.string), [ + 1, + 2, + 'b', + ]); + expectThrowsInProduction(PropTypes.setOf(PropTypes.string), { + '0': 'maybe-array', + length: 1, + }); + expectThrowsInProduction( + PropTypes.setOf(PropTypes.string).isRequired, + null, + ); + expectThrowsInProduction( + PropTypes.setOf(PropTypes.string).isRequired, + undefined, + ); + }); + }); + describe('Component Type', function() { it('should be a no-op', function() { expectThrowsInProduction(PropTypes.element, [
,
]); diff --git a/factoryWithThrowingShims.js b/factoryWithThrowingShims.js index e5b2f9c..2719f69 100644 --- a/factoryWithThrowingShims.js +++ b/factoryWithThrowingShims.js @@ -44,6 +44,10 @@ module.exports = function() { any: shim, arrayOf: getShim, + tupleOf: getShim, + iterableOf: getShim, + mapOf: getShim, + setOf: getShim, element: shim, elementType: shim, instanceOf: getShim, diff --git a/factoryWithTypeCheckers.js b/factoryWithTypeCheckers.js index adbd752..342b626 100644 --- a/factoryWithTypeCheckers.js +++ b/factoryWithTypeCheckers.js @@ -12,6 +12,7 @@ var assign = require('object-assign'); var ReactPropTypesSecret = require('./lib/ReactPropTypesSecret'); var has = require('./lib/has'); +var forEach = require('./lib/forEach'); var checkPropTypes = require('./checkPropTypes'); var printWarning = function() {}; @@ -35,6 +36,20 @@ function emptyFunctionThatReturnsNull() { return null; } +function isSet(maybeSet) { + try { + Object.getOwnPropertyDescriptor(Set.prototype, 'size').get.call(maybeSet); + return true + } catch(e) { return false; } +} + +function isMap(maybeMap) { + try { + Object.getOwnPropertyDescriptor(Map.prototype, 'size').get.call(maybeMap); + return true + } catch(e) { return false; } +} + module.exports = function(isValidElement, throwOnDirectAccess) { /* global Symbol */ var ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; @@ -123,6 +138,10 @@ module.exports = function(isValidElement, throwOnDirectAccess) { any: createAnyTypeChecker(), arrayOf: createArrayOfTypeChecker, + tupleOf: createTupleOfTypeChecker, + iterableOf: createIterableOfTypeChecker, + mapOf: createMapOfTypeChecker, + setOf: createSetOfTypeChecker, element: createElementTypeChecker(), elementType: createElementTypeTypeChecker(), instanceOf: createInstanceTypeChecker, @@ -269,6 +288,107 @@ module.exports = function(isValidElement, throwOnDirectAccess) { } return createChainableTypeChecker(validate); } + + function createTupleOfTypeChecker(typeCheckerTuple) { + var argsInvalid = !Array.isArray(typeCheckerTuple) || !typeCheckerTuple.every(function (checker) { return typeof checker === 'function'}); + + function validate(props, propName, componentName, location, propFullName) { + if (argsInvalid) { + return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside tupleOf.') + } + var propValue = props[propName]; + if (!Array.isArray(propValue)) { + var propType = getPropType(propValue); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + '`' + propType + '` supplied to `' + componentName + '`, expected an array.'); + } + if (propValue.length !== typeCheckerTuple.length) { + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of length ' + '`' + propValue.length + '` supplied to `' + componentName + '`, expected an array of length `' + typeCheckerTuple.length + '`.'); + } + for (var i = 0; i < typeCheckerTuple.length; i++) { + var typeChecker = typeCheckerTuple[i]; + var error = typeChecker(propValue, i, componentName, location, propFullName + '[' + i + ']', ReactPropTypesSecret); + if (error instanceof Error) { + return error; + } + } + return null; + } + return createChainableTypeChecker(validate); + } + + function createIterableOfTypeChecker(typeChecker) { + function validate(props, propName, componentName, location, propFullName) { + if (typeof typeChecker !== 'function') { + return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside iterableOf.'); + } + if (!(Symbol && Symbol.iterator)) { + return new PropTypeError('Cannot use `PropTypes.iterableOf` if Symbol.iterator is not present'); + } + var error; + var propValue = props[propName]; + + if (typeof propValue[Symbol.iterator] !== 'function') { + var propType = getPropType(propValue); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + '`' + propType + '` supplied to `' + componentName + '`, expected an iterable.'); + } + + forEach(propValue, function (value) { + var _error = typeChecker([value], 0, componentName, location, propFullName + '', ReactPropTypesSecret); + if (_error instanceof Error) { + error = _error; + return + } + }); + + return error instanceof Error ? error : null; + } + return createChainableTypeChecker(validate); + } + + function createMapOfTypeChecker(typeCheckers) { + var validateIterable = createIterableOfTypeChecker(createTupleOfTypeChecker(typeCheckers)); + + function validate(props, propName, componentName, location, propFullName) { + if (!Array.isArray(typeCheckers)) { + return new PropTypeError('Correct syntax is `PropTypes.mapOf([PropTypes.keyType, PropTypes.valueType])`.'); + } + var keyChecker = typeCheckers[0]; + var valueChecker = typeCheckers[1]; + if (typeof Map !== 'function') { + return new PropTypeError('Cannot use `PropTypes.mapOf` if the Map data structure is not present'); + } + if (typeof keyChecker !== 'function' || typeof valueChecker !== 'function') { + return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside mapOf.'); + } + var propValue = props[propName]; + if (!isMap(propValue)) { + var propType = getPropType(propValue); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a Map.')); + } + return validateIterable(props, propName, componentName, location, propFullName, ReactPropTypesSecret); + } + return createChainableTypeChecker(validate); + } + + function createSetOfTypeChecker(typeChecker) { + var validateIterable = createIterableOfTypeChecker(typeChecker); + + function validate(props, propName, componentName, location, propFullName) { + if (typeof Set !== 'function') { + return new PropTypeError('Cannot use `PropTypes.setOf` if the Set data structure is not present'); + } + if (typeof typeChecker !== 'function') { + return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside setOf.'); + } + var propValue = props[propName]; + if (!isSet(propValue)) { + var propType = getPropType(propValue); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a Set.')); + } + return validateIterable(props, propName, componentName, location, propFullName, ReactPropTypesSecret); + } + return createChainableTypeChecker(validate); + } function createElementTypeChecker() { function validate(props, propName, componentName, location, propFullName) { diff --git a/lib/forEach.js b/lib/forEach.js new file mode 100644 index 0000000..c1cffb1 --- /dev/null +++ b/lib/forEach.js @@ -0,0 +1,9 @@ +module.exports = function forEach(iterable, callback) { + var iterator = iterable[Symbol.iterator](); + + while(true) { + var step = iterator.next() + if (step.done) break; + callback(step.value); + } +}