PropTypes check of object with dynamic keys
React has lots of ways of using PropTypes to check the value of a prop. One that I commonly use is React.PropTypes.shape({...})
. However, I recently came across a situation where I have an object that will have dynamic key/values inside. I know that each key should be a string (in a known format), and each value should be an int. Even using a custom prop validation function, it still assumes you know the key of the prop. How do I use PropTypes to check that both the keys and values of an object/shape are correct?
...
someArray: React.PropTypes.arrayOf(React.PropTypes.shape({
// How to specify a dynamic string key? Keys are a date/datetime string
<dynamicStringKey>: React.PropTypes.number
}))
...
So again: I want to at the very least check that the value of each key is a number. Ideally, I would also like to be able to check the the key itself is a string in the correct format.
Solution 1:
To validate only the values, you can use React.PropTypes.objectOf
.
...
someArray: React.PropTypes.arrayOf(
React.PropTypes.objectOf(React.PropTypes.number)
)
...
Solution 2:
Note: This answer was written in 2015 when the current version of React was 0.14.3. It may or may not apply to the version of React you're using today.
That's an interesting question. From your question it sounds like you've read about custom type checkers in the docs for Prop Validation. For posterity I'll reproduce it here:
// You can also specify a custom validator. It should return an Error // object if the validation fails. Don't `console.warn` or throw, as this // won't work inside `oneOfType`. customProp: function(props, propName, componentName) { if (!/matchme/.test(props[propName])) { return new Error('Validation failed!'); } }
When implementing type checkers I prefer to use React's built-in type
checkers as much as possible. You want to check if the values are
numbers, so we should use PropTypes.number
for that, right? It would
be nice if we could just do PropTypes.number('not a number!')
and get
the appropriate error, but unfortunately it's a little more involved
than that. The first stop is to understand...
How type checkers work
Here's the function signature of a type checker:
function(props, propName, componentName, location, propFullName) => null | Error
As you can see, all of the props are passed as the first argument and
the name of the prop being tested is passed as the second. The last
three arguments are used for printing out useful error messages and are
optional: componentName
is self-explanatory. location
will be one of
'prop'
, 'context'
, or 'childContext'
(we're only interested in
'prop'
), and propFullName
is for when we're dealing with nested
props, e.g. someObj.someKey
.
Armed with this knowledge, we can now invoke a type checker directly:
PropTypes.number({ myProp: 'bad' }, 'myProp');
// => [Error: Invalid undefined `myProp` of type `string` supplied
// to `<<anonymous>>`, expected `number`.]
See? Not quite as useful without all of the arguments. This is better:
PropTypes.number({ myProp: 'bad' }, 'myProp', 'MyComponent', 'prop')
// => [Error: Invalid prop `myProp` of type `string` supplied
// to `MyComponent`, expected `number`.]
An array type checker
One thing the docs don't mention is that when you supply a custom type
checker to PropTypes.arrayOf
, it will be called for each array
element, and the first two arguments will be the array itself and the
current element's index, respectively. Now we can start sketching out
our type checker:
function validArrayItem(arr, idx, componentName, location, propFullName) {
var obj = arr[idx];
console.log(propFullName, obj);
// 1. Check if `obj` is an Object using `PropTypes.object`
// 2. Check if all of its keys conform to some specified format
// 3. Check if all of its values are numbers
return null;
}
So far it'll always return null
(which indicates valid props), but we
threw in a console.log
to get a peek at what's going on. Now we can
test it like this:
var typeChecker = PropTypes.arrayOf(validArrayItem);
var myArray = [ { foo: 1 }, { bar: 'qux' } ];
var props = { myProp: myArray };
typeChecker(props, 'myProp', 'MyComponent', 'prop');
// -> myProp[0] { foo: 1 }
// myProp[1] { bar: 'qux' }
// => null
As you can see, propFullName
is myProp[0]
for the first item and
myProp[1]
for the second.
Now let's flesh out the three parts of the function.
1. Check if obj
is an Object using PropTypes.object
This is the easiest part:
function validArrayItem(arr, idx, componentName, location, propFullName) {
var obj = arr[idx];
var props = {};
props[propFullName] = obj;
// Check if `obj` is an Object using `PropTypes.object`
var isObjectError = PropTypes.object(props, propFullName, componentName, location);
if (isObjectError) { return isObjectError; }
return null;
}
var typeChecker = PropTypes.arrayOf(validArrayItem);
var props = { myProp: [ { foo: 1 }, 'bar' ] };
typeChecker(props, 'myProp', 'MyComponent', 'prop');
// => [Error: Invalid prop `myProp[1]` of type `string` supplied to
// `MyComponent`, expected `object`.]
Perfect! Next...
2. Check if all of its keys conform to some specified format
In your question you say "each key should be a string," but all object keys in JavaScript are strings, so let's say, arbitrarily, that we want to test if the keys all start with a capital letter. Let's make a custom type checker for that:
var STARTS_WITH_UPPERCASE_LETTER_EXPR = /^[A-Z]/;
function validObjectKeys(props, propName, componentName, location, propFullName) {
var obj = props[propName];
var keys = Object.keys(obj);
// If the object is empty, consider it valid
if (keys.length === 0) { return null; }
var key;
var propFullNameWithKey;
for (var i = 0; i < keys.length; i++) {
key = keys[i];
propFullNameWithKey = (propFullName || propName) + '.' + key;
if (STARTS_WITH_UPPERCASE_LETTER_EXPR.test(key)) { continue; }
return new Error(
'Invalid key `' + propFullNameWithKey + '` supplied to ' +
'`' + componentName + '`; expected to match ' +
STARTS_WITH_UPPERCASE_LETTER_EXPR + '.'
);
}
return null;
}
We can test it on its own:
var props = { myProp: { Foo: 1, bar: 2 } };
validObjectKeys(props, 'myProp', 'MyComponent', 'prop');
// -> myProp.Foo Foo
// myProp.bar bar
// => [Error: Invalid key `myProp.bar` supplied to `MyComponent`;
// expected to match /^[A-Z]/.]
Great! Let's integrate it into our validArrayItem
type checker:
function validArrayItem(arr, idx, componentName, location, propFullName) {
var obj = arr[idx];
var props = {};
props[propFullName] = obj;
// Check if `obj` is an Object using `PropTypes.object`
var isObjectError = PropTypes.object(props, propFullName, componentName, location);
if (isObjectError) { return isObjectError; }
// Check if all of its keys conform to some specified format
var validObjectKeysError = validObjectKeys(props, propFullName, componentName);
if (validObjectKeysError) { return validObjectKeysError; }
return null;
}
And test it out:
var props = { myProp: [ { Foo: 1 }, { bar: 2 } ] };
var typeChecker = PropTypes.arrayOf(validArrayItem);
typeChecker(props, 'myProp', 'MyComponent', 'prop');
// -> myProp[0].Foo Foo
// myProp[1].bar bar
// => [Error: Invalid key `myProp[1].bar` supplied to `MyComponent`;
// expected to match /^[A-Z]/.]
And finally...
3. Check if all of its values are numbers
Happily, we don't need to do much work here, since we can use the
built-in PropTypes.objectOf
:
// Check if all of its values are numbers
var validObjectValues = PropTypes.objectOf(PropTypes.number);
var validObjectValuesError = validObjectValues(props, propFullName, componentName, location);
if (validObjectValuesError) { return validObjectValuesError; }
We'll test it below.
All together now
Here's our final code:
function validArrayItem(arr, idx, componentName, location, propFullName) {
var obj = arr[idx];
var props = {};
props[propFullName] = obj;
// Check if `obj` is an Object using `PropTypes.object`
var isObjectError = PropTypes.object(props, propFullName, componentName, location);
if (isObjectError) { return isObjectError; }
// Check if all of its keys conform to some specified format
var validObjectKeysError = validObjectKeys(props, propFullName, componentName);
if (validObjectKeysError) { return validObjectKeysError; }
// Check if all of its values are numbers
var validObjectValues = PropTypes.objectOf(PropTypes.number);
var validObjectValuesError = validObjectValues(props, propFullName, componentName, location);
if (validObjectValuesError) { return validObjectValuesError; }
return null;
}
We'll write a quick convenience function for testing and throw some data at it:
function test(arrayToTest) {
var typeChecker = PropTypes.arrayOf(validArrayItem);
var props = { testProp: arrayToTest };
return typeChecker(props, 'testProp', 'MyComponent', 'prop');
}
test([ { Foo: 1 }, { Bar: 2 } ]);
// => null
test([ { Foo: 1 }, { bar: 2 } ]);
// => [Error: Invalid key `testProp[1].bar` supplied to `MyComponent`;
// expected to match /^[A-Z]/.]
test([ { Foo: 1 }, { Bar: false } ]);
// => [Error: Invalid prop `testProp[1].Bar` of type `boolean` supplied to
// `MyComponent`, expected `number`.]
It works! Now you can use it in your React component just like the built-in type checkers:
MyComponent.propTypes = {
someArray: PropTypes.arrayOf(validArrayItem);
};
Of course, I would recommend giving it a more meaningful name and moving it into its own module.