facebook / prop-types

Runtime type checking for React props and similar objects
MIT License
4.48k stars 356 forks source link

Documentation on implementing tagged union validation #294

Closed 414owen closed 5 years ago

414owen commented 5 years ago

Algebraic data types, or tagged unions, represent a choice of multiple shapes.

Say I have an algebraic datatype which can take one of two forms:

// first form
{ type: 'haskeller'
, data:
    { favouriteMonad: 'Tardis'
    }
}
// second form
{ type: 'javascripter'
, data:
    { favouriteLoop: 'forOf'
    , favouriteLibrary: 'lodash'
    }
}

Where type is the discriminant.

At the moment, I'm validating this using:

PropTypes.oneOfType(
  [ { type: PropTypes.oneOf(['haskeller']).isRequired
    , data: PropTypes.shape(
        { favouriteMonad: PropTypes.oneOf([ /* etc */ ]),.isRequired
        }
      ).isRequired
    }
  , { type: PropTypes.oneOf(['javascripter']).isRequired
    , data: PropTypes.shape(
        { favouriteLoop: PropTypes.oneOf([ /* etc */ ]).isRequired
        , favouriteLibrary: PropTypes.oneOf([ /* etc */ ]).isRequired
        }
      ).isRequired
    }
  ]
).isRequired

So several things here:

ljharb commented 5 years ago

Based on the two forms you want, I'd expect this (which is very close to what you have):

PropTypes.oneOfType([
  PropTypes.shape({
    type: PropTypes.oneOf(['haskeller']).isRequired,
    data: PropTypes.shape({
      favoriteMonad: PropTypes.string.isRequired,
    }).isRequired,
  }),
  PropTypes.shape({
    type: PropTypes.oneOf(['javascripter']).isRequired,
    data: PropTypes.shape({
      favoriteLoop: PropTypes.string.isRequired,
      favoriteLibrary: PropTypes.string.isRequired,
    }).isRequired,
  }),
])

If you had the two shapes defined separately, it becomes much more readable:

const haskeller = PropTypes.shape({
  type: PropTypes.oneOf(['haskeller']).isRequired,
  data: PropTypes.shape({
    favoriteMonad: PropTypes.string.isRequired,
  }).isRequired,
});

const javascripter = PropTypes.shape({
  type: PropTypes.oneOf(['javascripter']).isRequired,
  data: PropTypes.shape({
    favoriteLoop: PropTypes.string.isRequired,
    favoriteLibrary: PropTypes.string.isRequired,
  }).isRequired,
});

…

PropTypes.oneOfType([haskeller, javascripter])
414owen commented 5 years ago

Thanks @ljharb. It might be nice to have something similar in the docs, I find it hard to imagine it's an uncommon use case.

ljharb commented 5 years ago

A PR to improve the docs is always welcome!