Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PropTypes check of object with dynamic keys

Tags:

reactjs

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.

like image 540
Matthew Herbst Avatar asked Dec 18 '15 19:12

Matthew Herbst


People also ask

What does the PropTypes check do for the component?

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.

Are PropTypes deprecated?

PropTypes is deprecated since React 15.5. 0, use the npm module prop-types instead .

Can we use PropTypes in functional component?

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.

What is the difference between flow and PropTypes?

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.


2 Answers

To validate only the values, you can use React.PropTypes.objectOf.

... someArray: React.PropTypes.arrayOf(   React.PropTypes.objectOf(React.PropTypes.number) ) ... 
like image 103
Daniel Garcia Avatar answered Sep 29 '22 07:09

Daniel Garcia


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.

like image 42
Jordan Running Avatar answered Sep 29 '22 08:09

Jordan Running