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.
PropTypes are simply a mechanism that ensures that the passed value is of the correct datatype. This makes sure that we don't receive an error at the very end of our app by the console which might not be easy to deal with.
PropTypes is deprecated since React 15.5. 0, use the npm module prop-types instead .
In this example, we are using a class component, but the same functionality could also be applied to function components, or components created by React.memo or React.forwardRef . PropTypes exports a range of validators that can be used to make sure the data you receive is valid.
Flow is a static analysis tool which uses a superset of the language, allowing you to add type annotations to all of your code and catch an entire class of bugs at compile time. PropTypes is a basic type checker which has been patched onto React.
To validate only the values, you can use React.PropTypes.objectOf
.
... someArray: React.PropTypes.arrayOf( React.PropTypes.objectOf(React.PropTypes.number) ) ...
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...
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`.]
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.
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...
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...
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With