@samitouri / QOS-React-2 / commits / 697a9113c0

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.

Marshall Roch committed Nov 27, 2013 at 12:01 UTC 697a9113c0a391e60dc4b9f797213d8db6da919d
2 files changed +138 -25
src/core/ReactPropTypes.js
+80 -25
@@ -81,6 +81,7 @@ var Props = {
81 string: createPrimitiveTypeChecker('string'),
82
83 oneOf: createEnumTypeChecker,
84 + oneOfType: createUnionTypeChecker,
85
86 instanceOf: createInstanceTypeChecker
87
@@ -89,13 +90,19 @@ var Props = {
90 var ANONYMOUS = '<<anonymous>>';
91
92 function createPrimitiveTypeChecker(expectedType) {
92 - function validatePrimitiveType(propValue, propName, componentName, location) {
93 + function validatePrimitiveType(
94 + shouldThrow, propValue, propName, componentName, location
95 + ) {
96 var propType = typeof propValue;
97 if (propType === 'object' && Array.isArray(propValue)) {
98 propType = 'array';
99 }
100 + var isValid = propType === expectedType;
101 + if (!shouldThrow) {
102 + return isValid;
103 + }
104 invariant(
98 - propType === expectedType,
105 + isValid,
106 'Invalid %s `%s` of type `%s` supplied to `%s`, expected `%s`.',
107 ReactPropTypeLocationNames[location],
108 propName,
@@ -109,9 +116,15 @@ function createPrimitiveTypeChecker(expectedType) {
116
117 function createEnumTypeChecker(expectedValues) {
118 var expectedEnum = createObjectFrom(expectedValues);
112 - function validateEnumType(propValue, propName, componentName, location) {
119 + function validateEnumType(
120 + shouldThrow, propValue, propName, componentName, location
121 + ) {
122 + var isValid = expectedEnum[propValue];
123 + if (!shouldThrow) {
124 + return isValid;
125 + }
126 invariant(
114 - expectedEnum[propValue],
127 + isValid,
128 'Invalid %s `%s` supplied to `%s`, expected one of %s.',
129 ReactPropTypeLocationNames[location],
130 propName,
@@ -123,9 +136,15 @@ function createEnumTypeChecker(expectedValues) {
136 }
137
138 function createInstanceTypeChecker(expectedClass) {
126 - function validateInstanceType(propValue, propName, componentName, location) {
139 + function validateInstanceType(
140 + shouldThrow, propValue, propName, componentName, location
141 + ) {
142 + var isValid = propValue instanceof expectedClass;
143 + if (!shouldThrow) {
144 + return isValid;
145 + }
146 invariant(
128 - propValue instanceof expectedClass,
147 + isValid,
148 'Invalid %s `%s` supplied to `%s`, expected instance of `%s`.',
149 ReactPropTypeLocationNames[location],
150 propName,
@@ -137,28 +156,64 @@ function createInstanceTypeChecker(expectedClass) {
156 }
157
158 function createChainableTypeChecker(validate) {
140 - function createTypeChecker(isRequired) {
141 - function checkType(props, propName, componentName, location) {
142 - var propValue = props[propName];
143 - if (propValue != null) {
144 - // Only validate if there is a value to check.
145 - validate(propValue, propName, componentName || ANONYMOUS, location);
146 - } else {
147 - invariant(
148 - !isRequired,
149 - 'Required %s `%s` was not specified in `%s`.',
150 - ReactPropTypeLocationNames[location],
151 - propName,
152 - componentName || ANONYMOUS
153 - );
159 + function checkType(
160 + isRequired, shouldThrow, props, propName, componentName, location
161 + ) {
162 + var propValue = props[propName];
163 + if (propValue != null) {
164 + // Only validate if there is a value to check.
165 + return validate(
166 + shouldThrow,
167 + propValue,
168 + propName,
169 + componentName || ANONYMOUS,
170 + location
171 + );
172 + } else {
173 + var isValid = !isRequired;
174 + if (!shouldThrow) {
175 + return isValid;
176 }
177 + invariant(
178 + isValid,
179 + 'Required %s `%s` was not specified in `%s`.',
180 + ReactPropTypeLocationNames[location],
181 + propName,
182 + componentName || ANONYMOUS
183 + );
184 }
156 - if (!isRequired) {
157 - checkType.isRequired = createTypeChecker(true);
158 - }
159 - return checkType;
185 }
161 - return createTypeChecker(false);
186 +
187 + var checker = checkType.bind(null, false, true);
188 + checker.weak = checkType.bind(null, false, false);
189 + checker.isRequired = checkType.bind(null, true, true);
190 + checker.weak.isRequired = checkType.bind(null, true, false);
191 + checker.isRequired.weak = checker.weak.isRequired;
192 +
193 + return checker;
194 +}
195 +
196 +function createUnionTypeChecker(arrayOfValidators) {
197 + return function(props, propName, componentName, location) {
198 + var isValid = false;
199 + for (var ii = 0; ii < arrayOfValidators.length; ii++) {
200 + var validate = arrayOfValidators[ii];
201 + if (typeof validate.weak === 'function') {
202 + validate = validate.weak;
203 + }
204 + if (validate(props, propName, componentName, location)) {
205 + isValid = true;
206 + break;
207 + }
208 + }
209 + invariant(
210 + isValid,
211 + 'Invalid %s `%s` supplied to `%s`.',
212 + ReactPropTypeLocationNames[location],
213 + propName,
214 + componentName || ANONYMOUS
215 + );
216 + };
217 }
218
219 module.exports = Props;
src/core/__tests__/ReactPropTypes-test.js
+58
@@ -84,6 +84,12 @@ describe('Primitive Types', function() {
84 '`testComponent`.'
85 );
86 });
87 +
88 + it("should have a weak version that returns true/false", function() {
89 + expect(typeCheck(Props.string.weak, null)()).toEqual(true);
90 + expect(typeCheck(Props.string.weak.isRequired, null)()).toEqual(false);
91 + expect(typeCheck(Props.string.isRequired.weak, null)()).toEqual(false);
92 + });
93 });
94
95 describe('Enum Types', function() {
@@ -115,6 +121,13 @@ describe('Enum Types', function() {
121 expect(typeCheck(Props.oneOf(['red', 'blue']), null)).not.toThrow();
122 expect(typeCheck(Props.oneOf(['red', 'blue']), undefined)).not.toThrow();
123 });
124 +
125 + it("should have a weak version that returns true/false", function() {
126 + var checker = Props.oneOf(['red', 'blue']);
127 + expect(typeCheck(checker.weak, null)()).toEqual(true);
128 + expect(typeCheck(checker.weak.isRequired, null)()).toEqual(false);
129 + expect(typeCheck(checker.isRequired.weak, null)()).toEqual(false);
130 + });
131 });
132
133 describe('Instance Types', function() {
@@ -148,3 +161,48 @@ describe('Instance Types', function() {
161 expect(typeCheck(Props.instanceOf(Person), new Engineer())).not.toThrow();
162 });
163 });
164 +
165 +describe('Union Types', function() {
166 + beforeEach(function() {
167 + require('mock-modules').dumpCache();
168 + });
169 +
170 + it('should throw if none of the types are valid', function() {
171 + var checker = Props.oneOfType([
172 + Props.string,
173 + Props.number
174 + ]);
175 + expect(typeCheck(checker, [])).toThrow(
176 + 'Invariant Violation: Invalid prop `testProp` ' +
177 + 'supplied to `testComponent`.'
178 + );
179 +
180 + checker = Props.oneOfType([
181 + Props.string.isRequired,
182 + Props.number.isRequired
183 + ]);
184 + expect(typeCheck(checker, null)).toThrow(
185 + 'Invariant Violation: Invalid prop `testProp` ' +
186 + 'supplied to `testComponent`.'
187 + );
188 + });
189 +
190 + it('should not throw if one of the types are valid', function() {
191 + var checker = Props.oneOfType([
192 + Props.string,
193 + Props.number
194 + ]);
195 + expect(typeCheck(checker, null)).not.toThrow();
196 + expect(typeCheck(checker, 'foo')).not.toThrow();
197 + expect(typeCheck(checker, 123)).not.toThrow();
198 +
199 + checker = Props.oneOfType([
200 + Props.string,
201 + Props.number.isRequired
202 + ]);
203 + expect(typeCheck(checker, null)).not.toThrow();
204 + expect(typeCheck(checker, 'foo')).not.toThrow();
205 + expect(typeCheck(checker, 123)).not.toThrow();
206 + });
207 +
208 +});