Skip to content

Commit 0292d34

Browse files
authored
PropTypes: distinguish nullable from optional object field (facebook#7291)
* PropTypes: distinguish nullable from optional object field This gives a more precise message (no type semantics change) to the case of passing a field in an object, but whose value is `null`: Before: ```js propTypes: { foo: React.PropTypes.number.isRequired } ``` Would scream "Required prop `foo` was not specified in `MyComp`". Now it'll be "Required prop `foo` was specified in `MyComp`, but its value is `null`.". Works as expected in nested objects. This fixes the issue of a component transitively passing a `null`, specifying the correct field to the child but have the child tell it that it didn't provide the prop. Optional field and nullable are two different things anyway. * Add missing test case. * Reword messages.
1 parent fe5128f commit 0292d34

File tree

7 files changed

+98
-96
lines changed

7 files changed

+98
-96
lines changed

src/addons/link/__tests__/ReactLinkPropTypes-test.js

+14-8
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ var ReactPropTypeLocations = require('ReactPropTypeLocations');
1818
var ReactPropTypesSecret = require('ReactPropTypesSecret');
1919

2020
var invalidMessage = 'Invalid prop `testProp` supplied to `testComponent`.';
21-
var requiredMessage =
22-
'Required prop `testProp` was not specified in `testComponent`.';
21+
var requiredMessage = 'The prop `testProp` is marked as required in ' +
22+
'`testComponent`, but its value is `undefined`.';
2323

2424
function typeCheckFail(declaration, value, message) {
2525
var props = {testProp: value};
@@ -53,22 +53,26 @@ describe('ReactLink', function() {
5353
typeCheckFail(
5454
LinkPropTypes.link(React.PropTypes.any),
5555
{},
56-
'Required prop `testProp.value` was not specified in `testComponent`.'
56+
'The prop `testProp.value` is marked as required in `testComponent`, ' +
57+
'but its value is `undefined`.'
5758
);
5859
typeCheckFail(
5960
LinkPropTypes.link(React.PropTypes.any),
6061
{value: 123},
61-
'Required prop `testProp.requestChange` was not specified in `testComponent`.'
62+
'The prop `testProp.requestChange` is marked as required in ' +
63+
'`testComponent`, but its value is `undefined`.'
6264
);
6365
typeCheckFail(
6466
LinkPropTypes.link(React.PropTypes.any),
6567
{requestChange: emptyFunction},
66-
'Required prop `testProp.value` was not specified in `testComponent`.'
68+
'The prop `testProp.value` is marked as required in `testComponent`, ' +
69+
'but its value is `undefined`.'
6770
);
6871
typeCheckFail(
6972
LinkPropTypes.link(React.PropTypes.any),
7073
{value: null, requestChange: null},
71-
'Required prop `testProp.value` was not specified in `testComponent`.'
74+
'The prop `testProp.value` is marked as required in `testComponent`, ' +
75+
'but its value is `null`.'
7276
);
7377
});
7478

@@ -122,12 +126,14 @@ describe('ReactLink', function() {
122126
});
123127

124128
it('should warn for missing required values', function() {
125-
typeCheckFail(LinkPropTypes.link().isRequired, null, requiredMessage);
129+
var specifiedButIsNullMsg = 'The prop `testProp` is marked as required ' +
130+
'in `testComponent`, but its value is `null`.';
131+
typeCheckFail(LinkPropTypes.link().isRequired, null, specifiedButIsNullMsg);
126132
typeCheckFail(LinkPropTypes.link().isRequired, undefined, requiredMessage);
127133
typeCheckFail(
128134
LinkPropTypes.link(React.PropTypes.string).isRequired,
129135
null,
130-
requiredMessage
136+
specifiedButIsNullMsg
131137
);
132138
typeCheckFail(
133139
LinkPropTypes.link(React.PropTypes.string).isRequired,

src/isomorphic/classic/__tests__/ReactContextValidator-test.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ describe('ReactContextValidator', function() {
152152
expect(console.error.calls.count()).toBe(1);
153153
expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
154154
'Warning: Failed context type: ' +
155-
'Required context `foo` was not specified in `Component`.\n' +
155+
'The context `foo` is marked as required in `Component`, but its value ' +
156+
'is `undefined`.\n' +
156157
' in Component (at **)'
157158
);
158159

@@ -229,7 +230,8 @@ describe('ReactContextValidator', function() {
229230
expect(console.error.calls.count()).toBe(1);
230231
expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
231232
'Warning: Failed childContext type: ' +
232-
'Required child context `foo` was not specified in `Component`.\n' +
233+
'The child context `foo` is marked as required in `Component`, but its ' +
234+
'value is `undefined`.\n' +
233235
' in Component (at **)'
234236
);
235237

src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -360,8 +360,8 @@ describe('ReactElementValidator', function() {
360360

361361
expect(console.error.calls.count()).toBe(1);
362362
expect(console.error.calls.argsFor(0)[0]).toBe(
363-
'Warning: Failed prop type: ' +
364-
'Required prop `prop` was not specified in `Component`.\n' +
363+
'Warning: Failed prop type: The prop `prop` is marked as required in ' +
364+
'`Component`, but its value is `null`.\n' +
365365
' in Component'
366366
);
367367
});
@@ -385,8 +385,8 @@ describe('ReactElementValidator', function() {
385385

386386
expect(console.error.calls.count()).toBe(1);
387387
expect(console.error.calls.argsFor(0)[0]).toBe(
388-
'Warning: Failed prop type: ' +
389-
'Required prop `prop` was not specified in `Component`.\n' +
388+
'Warning: Failed prop type: The prop `prop` is marked as required in ' +
389+
'`Component`, but its value is `null`.\n' +
390390
' in Component'
391391
);
392392
});
@@ -413,7 +413,8 @@ describe('ReactElementValidator', function() {
413413
expect(console.error.calls.count()).toBe(2);
414414
expect(console.error.calls.argsFor(0)[0]).toBe(
415415
'Warning: Failed prop type: ' +
416-
'Required prop `prop` was not specified in `Component`.\n' +
416+
'The prop `prop` is marked as required in `Component`, but its value ' +
417+
'is `undefined`.\n' +
417418
' in Component'
418419
);
419420

src/isomorphic/classic/types/ReactPropTypes.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,15 @@ function createChainableTypeChecker(validate) {
144144
if (props[propName] == null) {
145145
var locationName = ReactPropTypeLocationNames[location];
146146
if (isRequired) {
147+
if (props[propName] === null) {
148+
return new Error(
149+
`The ${locationName} \`${propFullName}\` is marked as required ` +
150+
`in \`${componentName}\`, but its value is \`null\`.`
151+
);
152+
}
147153
return new Error(
148-
`Required ${locationName} \`${propFullName}\` was not specified in ` +
149-
`\`${componentName}\`.`
154+
`The ${locationName} \`${propFullName}\` is marked as required in ` +
155+
`\`${componentName}\`, but its value is \`undefined\`.`
150156
);
151157
}
152158
return null;

src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js

+58-72
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ var ReactPropTypesSecret;
2020

2121
var Component;
2222
var MyComponent;
23-
var requiredMessage =
24-
'Required prop `testProp` was not specified in `testComponent`.';
2523

2624
function typeCheckFail(declaration, value, message) {
2725
var props = {testProp: value};
@@ -37,6 +35,46 @@ function typeCheckFail(declaration, value, message) {
3735
expect(error.message).toBe(message);
3836
}
3937

38+
function typeCheckFailRequiredValues(declaration) {
39+
var specifiedButIsNullMsg = 'The prop `testProp` is marked as required in ' +
40+
'`testComponent`, but its value is `null`.';
41+
var unspecifiedMsg = 'The prop `testProp` is marked as required in ' +
42+
'`testComponent`, but its value is \`undefined\`.';
43+
var props1 = {testProp: null};
44+
var error1 = declaration(
45+
props1,
46+
'testProp',
47+
'testComponent',
48+
ReactPropTypeLocations.prop,
49+
null,
50+
ReactPropTypesSecret
51+
);
52+
expect(error1 instanceof Error).toBe(true);
53+
expect(error1.message).toBe(specifiedButIsNullMsg);
54+
var props2 = {testProp: undefined};
55+
var error2 = declaration(
56+
props2,
57+
'testProp',
58+
'testComponent',
59+
ReactPropTypeLocations.prop,
60+
null,
61+
ReactPropTypesSecret
62+
);
63+
expect(error2 instanceof Error).toBe(true);
64+
expect(error2.message).toBe(unspecifiedMsg);
65+
var props3 = {};
66+
var error3 = declaration(
67+
props3,
68+
'testProp',
69+
'testComponent',
70+
ReactPropTypeLocations.prop,
71+
null,
72+
ReactPropTypesSecret
73+
);
74+
expect(error3 instanceof Error).toBe(true);
75+
expect(error3.message).toBe(unspecifiedMsg);
76+
}
77+
4078
function typeCheckPass(declaration, value) {
4179
var props = {testProp: value};
4280
var error = declaration(
@@ -146,8 +184,7 @@ describe('ReactPropTypes', function() {
146184
});
147185

148186
it('should warn for missing required values', function() {
149-
typeCheckFail(PropTypes.string.isRequired, null, requiredMessage);
150-
typeCheckFail(PropTypes.string.isRequired, undefined, requiredMessage);
187+
typeCheckFailRequiredValues(PropTypes.string.isRequired);
151188
});
152189

153190
it('should warn if called manually in development', function() {
@@ -213,8 +250,7 @@ describe('ReactPropTypes', function() {
213250
});
214251

215252
it('should warn for missing required values', function() {
216-
typeCheckFail(PropTypes.any.isRequired, null, requiredMessage);
217-
typeCheckFail(PropTypes.any.isRequired, undefined, requiredMessage);
253+
typeCheckFailRequiredValues(PropTypes.any.isRequired);
218254
});
219255

220256
it('should warn if called manually in development', function() {
@@ -307,15 +343,8 @@ describe('ReactPropTypes', function() {
307343
});
308344

309345
it('should warn for missing required values', function() {
310-
typeCheckFail(
311-
PropTypes.arrayOf(PropTypes.number).isRequired,
312-
null,
313-
requiredMessage
314-
);
315-
typeCheckFail(
316-
PropTypes.arrayOf(PropTypes.number).isRequired,
317-
undefined,
318-
requiredMessage
346+
typeCheckFailRequiredValues(
347+
PropTypes.arrayOf(PropTypes.number).isRequired
319348
);
320349
});
321350

@@ -406,8 +435,7 @@ describe('ReactPropTypes', function() {
406435
});
407436

408437
it('should warn for missing required values', function() {
409-
typeCheckFail(PropTypes.element.isRequired, null, requiredMessage);
410-
typeCheckFail(PropTypes.element.isRequired, undefined, requiredMessage);
438+
typeCheckFailRequiredValues(PropTypes.element.isRequired);
411439
});
412440

413441
it('should warn if called manually in development', function() {
@@ -493,12 +521,7 @@ describe('ReactPropTypes', function() {
493521
});
494522

495523
it('should warn for missing required values', function() {
496-
typeCheckFail(
497-
PropTypes.instanceOf(String).isRequired, null, requiredMessage
498-
);
499-
typeCheckFail(
500-
PropTypes.instanceOf(String).isRequired, undefined, requiredMessage
501-
);
524+
typeCheckFailRequiredValues(PropTypes.instanceOf(String).isRequired);
502525
});
503526

504527
it('should warn if called manually in development', function() {
@@ -601,16 +624,7 @@ describe('ReactPropTypes', function() {
601624
});
602625

603626
it('should warn for missing required values', function() {
604-
typeCheckFail(
605-
PropTypes.node.isRequired,
606-
null,
607-
'Required prop `testProp` was not specified in `testComponent`.'
608-
);
609-
typeCheckFail(
610-
PropTypes.node.isRequired,
611-
undefined,
612-
'Required prop `testProp` was not specified in `testComponent`.'
613-
);
627+
typeCheckFailRequiredValues(PropTypes.node.isRequired);
614628
});
615629

616630
it('should accept empty array for required props', function() {
@@ -724,15 +738,8 @@ describe('ReactPropTypes', function() {
724738
});
725739

726740
it('should warn for missing required values', function() {
727-
typeCheckFail(
728-
PropTypes.objectOf(PropTypes.number).isRequired,
729-
null,
730-
requiredMessage
731-
);
732-
typeCheckFail(
733-
PropTypes.objectOf(PropTypes.number).isRequired,
734-
undefined,
735-
requiredMessage
741+
typeCheckFailRequiredValues(
742+
PropTypes.objectOf(PropTypes.number).isRequired
736743
);
737744
});
738745

@@ -804,16 +811,7 @@ describe('ReactPropTypes', function() {
804811
});
805812

806813
it('should warn for missing required values', function() {
807-
typeCheckFail(
808-
PropTypes.oneOf(['red', 'blue']).isRequired,
809-
null,
810-
requiredMessage
811-
);
812-
typeCheckFail(
813-
PropTypes.oneOf(['red', 'blue']).isRequired,
814-
undefined,
815-
requiredMessage
816-
);
814+
typeCheckFailRequiredValues(PropTypes.oneOf(['red', 'blue']).isRequired);
817815
});
818816

819817
it('should warn if called manually in development', function() {
@@ -882,15 +880,8 @@ describe('ReactPropTypes', function() {
882880
});
883881

884882
it('should warn for missing required values', function() {
885-
typeCheckFail(
886-
PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
887-
null,
888-
requiredMessage
889-
);
890-
typeCheckFail(
891-
PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
892-
undefined,
893-
requiredMessage
883+
typeCheckFailRequiredValues(
884+
PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired
894885
);
895886
});
896887

@@ -950,7 +941,8 @@ describe('ReactPropTypes', function() {
950941
typeCheckFail(
951942
PropTypes.shape({key: PropTypes.number.isRequired}),
952943
{},
953-
'Required prop `testProp.key` was not specified in `testComponent`.'
944+
'The prop `testProp.key` is marked as required in `testComponent`, ' +
945+
'but its value is `undefined`.'
954946
);
955947
});
956948

@@ -961,7 +953,8 @@ describe('ReactPropTypes', function() {
961953
secondKey: PropTypes.number.isRequired,
962954
}),
963955
{},
964-
'Required prop `testProp.key` was not specified in `testComponent`.'
956+
'The prop `testProp.key` is marked as required in `testComponent`, ' +
957+
'but its value is `undefined`.'
965958
);
966959
});
967960

@@ -983,15 +976,8 @@ describe('ReactPropTypes', function() {
983976
});
984977

985978
it('should warn for missing required values', function() {
986-
typeCheckFail(
987-
PropTypes.shape({key: PropTypes.number}).isRequired,
988-
null,
989-
requiredMessage
990-
);
991-
typeCheckFail(
992-
PropTypes.shape({key: PropTypes.number}).isRequired,
993-
undefined,
994-
requiredMessage
979+
typeCheckFailRequiredValues(
980+
PropTypes.shape({key: PropTypes.number}).isRequired
995981
);
996982
});
997983

src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,8 @@ describe('ReactJSXElementValidator', function() {
249249
expect(
250250
console.error.calls.argsFor(0)[0].replace(/\(at .+?:\d+\)/g, '(at **)')
251251
).toBe(
252-
'Warning: Failed prop type: ' +
253-
'Required prop `prop` was not specified in `RequiredPropComponent`.\n' +
252+
'Warning: Failed prop type: The prop `prop` is marked as required in ' +
253+
'`RequiredPropComponent`, but its value is `null`.\n' +
254254
' in RequiredPropComponent (at **)'
255255
);
256256
});
@@ -264,8 +264,8 @@ describe('ReactJSXElementValidator', function() {
264264
expect(
265265
console.error.calls.argsFor(0)[0].replace(/\(at .+?:\d+\)/g, '(at **)')
266266
).toBe(
267-
'Warning: Failed prop type: ' +
268-
'Required prop `prop` was not specified in `RequiredPropComponent`.\n' +
267+
'Warning: Failed prop type: The prop `prop` is marked as required in ' +
268+
'`RequiredPropComponent`, but its value is `null`.\n' +
269269
' in RequiredPropComponent (at **)'
270270
);
271271
});
@@ -281,7 +281,8 @@ describe('ReactJSXElementValidator', function() {
281281
console.error.calls.argsFor(0)[0].replace(/\(at .+?:\d+\)/g, '(at **)')
282282
).toBe(
283283
'Warning: Failed prop type: ' +
284-
'Required prop `prop` was not specified in `RequiredPropComponent`.\n' +
284+
'The prop `prop` is marked as required in `RequiredPropComponent`, but ' +
285+
'its value is `undefined`.\n' +
285286
' in RequiredPropComponent (at **)'
286287
);
287288

src/test/__tests__/ReactTestUtils-test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -296,8 +296,8 @@ describe('ReactTestUtils', function() {
296296
expect(
297297
console.error.calls.argsFor(0)[0].replace(/\(at .+?:\d+\)/g, '(at **)')
298298
).toBe(
299-
'Warning: Failed context type: Required context `name` was not ' +
300-
'specified in `SimpleComponent`.\n' +
299+
'Warning: Failed context type: The context `name` is marked as ' +
300+
'required in `SimpleComponent`, but its value is `undefined`.\n' +
301301
' in SimpleComponent (at **)'
302302
);
303303
});

0 commit comments

Comments
 (0)