Giulio Canti

Learn about me or read more of my blog


An alternative syntax for React propTypes

Written by Giulio Canti on 10 Sep 2014

Should I add propTypes to my components?

Definitely yes. Adding propTypes to all the components improves development speed, testability and documents the shape of the consumed data. After two months who can’t even remember which property feeds which component?

But in order to become an ubiquitous practice, the syntax used to express the constraints must be simple and expressive, otherwise chances are that laziness leads to undocumented and opaque components.

The tools provided by React are good, but when the domain models become more complex and you want express fine-grained constraints you can hit a wall.

The case of the Alert component

Say you are writing a component that represents the Alert component of Bootstrap and you want to check its props. There is already a fine implementation, so let’s assume that as base. Here a short list of handled props:

  • bsStyle - required, one of info, warning, success, danger
  • bsSize - optional, one of large, medium, small, xsmall
  • onDismiss - optional, a function called if the component is closable
  • dismissAfter - optional positive integer, millis to wait before closing the Alert. If specified then onDismiss must be specified too

Writing propTypes

var pt = require('react').PropTypes;

var propTypes = {

  bsStyle: pt.oneOf(['info', 'warning', 'success', 'danger']).isRequired,

  bsSize: pt.oneOf(['large', 'medium', 'small', 'xsmall']),

  onDismiss: pt.func,

  dismissAfter: function(props) {
    var n = props.dismissAfter;

    // dismissAfter is optional
    if (n != null) {

      // dismissAfter should be a positive integer
      if (typeof n !== 'number' || n !== parseInt(n, 10) || n < 0) {
        return new Error('Invalid `dismissAfter` supplied to `Alert`' +
          ', expected a positive integer');
      }

      // if specified then `onDismiss` must be specified too
      if (typeof props.onDismiss !== 'function') {
        return new Error('Invalid `onDismiss` supplied to `Alert`' +
          ', expected a func when `dismissAfter` is specified');
      }
    }
  }

};

Notes:

  1. by default props are optionals
  2. sometimes React issues cryptic warning messages
  3. there is no easy way to whitelist props
  4. refinements and global contraints must be specified on prop level with custom props

Proposed syntax

var t = require('tcomb-react');

var BsStyle = t.enums.of('info warning success danger');

var BsSize = t.enums.of('large medium small xsmall');

// a predicate is a function with signature (x) -> boolean
function predicate(n) { return n === parseInt(n, 10) && n >= 0; }

var PositiveInt = t.subtype(t.Num, predicate);

function globalPredicate(x) {
  return !( !t.Nil.is(x.dismissAfter) && t.Nil.is(x.onDismiss) );
}

var AlertProps = t.subtype(t.struct({
  bsStyle:      BsStyle,
  bsSize:       t.maybe(BsSize), // `maybe` means optional
  onDismiss:    t.maybe(t.Func),
  dismissAfter: t.maybe(PositiveInt)
}, globalPredicate);

// `bind` returns a proxy component with the same interface of the original component
// but with asserts included. In production you can choose to switch to the original one
var SafeAlert = t.react.bind(Alert, AlertProps, {strict: true});

Features:

  1. by default props are required, a saner default since it’s quite easy to forget .isRequired
  2. when a validation fails, the debugger kicks in so you can inspect the stack and quickly find out what’s wrong
  3. {strict: true} means all unspecified props are not allowed
  4. global contraints can be specified with subtype syntax
  5. plus: all defined types can be reused as domain models

Implementation

For a complete implementation of the ideas exposed in this post see the tcomb-react library on GitHub.

Playground

If you want to see tcomb-react in action, check out the playground of tcomb-react-bootstrap, an attempt to add a type checking layer to the components of react-bootstrap.

Comparison table

</thead> </tr> </tr> </tr> </tr> </tr> </tr> </tr> </tr> </tr> </tr> </tr> </tr> </tr> </tr> </tbody> </table>
Desc React Proposed syntax
optional prop A maybe(A)
required prop A.isRequired A
primitives array
bool
func
number
object
string




Arr
Bool
Func
Num
Obj
Str
Nil - null, undefined
Re - RegExp
Dat - Date
Err - Error
tuples tuple([A, B])
subtypes subtype(A, predicate)
all any Any
lists arrayOf(A) list(A)
components component Component
instance instanceOf(A) A
dictionaries objectOf(A) Dict(A)
enums oneOf(['a', 'b']) enums.of('a b')
unions oneOfType([A, B]) union([A, B])
renderable renderable Renderable
duck typing shape struct
comments powered by Disqus