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
+});