vanilla-prop-types
v2.0.0
Published
simple prop-types checker
Downloads
35
Readme
vanilla-prop-types
vanilla-prop-types is an approximate implemententation of prop-types
that can be used in ES5 environments, NON object-oriented code, and with NO dependencies upon React
(the only dependencies are lodash
and is-callable
). It is intended to be used as an effective props type-checker tool.
this module has four primary module components:
PropTypes
- syntax carried over from React prop-typescheckProps
- returns a string validation message if invalid, returnsnull
otherwisevalidateProps
- throwsError
if invalid (with the same validation message), does nothing otherwiseareValidProps
- returns a strict boolean (return true if and only if checkProps version return null)
usage
const {
PropTypes,
checkProps,
validateProps,
areValidProps,
} = require('vanilla-prop-types');
// create the check function
const allProps = {
propA: PropTypes.number,
propB: PropTypes.number.isRequired,
propC: PropTypes.number.isRequiredOrNull,
propD: PropTypes.shape({
shA: PropTypes.array,
shB: PropTypes.arrayOf(PropTypes.string),
shC: PropTypes.shape({
subshA: PropTypes.symbol.isRequired,
}).isRequired,
}),
propE: PropTypes.custom(({prop, propName, props}) => {
// returning a string from a custom sets the validation error message
if (prop === 'derp') {
return 'propE should never be derp!!!!',
}
}),
propF: PropTypes.bool.isRequired.custom(({prop, propName, props}) => {
// throwing an error also sets the validation error message
if (!prop) {
throw new Error('propF should not have be false');
}
}),
propG: PropTypes.number.isRequiredOrNull.custom(({prop, propName, props}) => {
// otherwise, returning anything TRUTHY is interpreted as a validation fail
// and will generate a default error message
// >>> custom validation function fails for propName "propG"
if (prop === 42) {
return true;
}
}),
};
// return a STRING value or NULL
const check = checkProps(allProps);
// throws error when invalid
const validate = validateProps(allProps);
// use the strict boolean (will NEVER throw)
const isValid = areValidProps(allProps);
// a tyical usage case: validating function input
const myFunc1 = (arg) => {
const errMessage = check(arg);
if (errMessage) {
throw new Error(`invalid props: ${errMessage}`);
}
if (isValid(arg)) {
console.log('the arg is valid!')
}
try {
validate(arg);
} catch (err) {
assert(err.message === check(arg))
}
};
// it can also be effectively used to compose new shapes on the spot
const validateArgs = validateProps({
apple: PropTypes.string.isRequired,
banana: PropTypes.string.isRequired,
isNew: PropTypes
});
const myFunc2 = (apple, banana, {isOld, isNew}) => {
validate({
apple,
propB,
isNew,
isOld,
});
}
Differences from React prop-types
vanilla-prop-types
does differ from prop-types
in a few significant ways, chiefly
checkProps
function can take an optional 2ndoptions
argvalidateProps
function can take an optional 2ndoptions
argareValidProps
function can take an optional 2ndoptions
argPropTypes.custom
has 1 arg{prop, propName, props}
(and raw custom functions are not allowed)- a
custom
function check can be added to any normal propType (e.g.PropTypes.number.isRequired.custom(() => 0)
) - the
isRequiredOrNull
extension is added isExact
options for both the outermost object, as well asPropTypes.shape
checkProps validateProps & areValidProps
takes two arguments, and returns either a string message or null
const check = checkProps(propTypes, options);
const validate = validate(propTypes, options);
// lodash is used below for some example code, but is not generally needed
const _ = require('lodash');
options arg
where (optional) options object has the following propTypes:
const optionsTypes = {
isNullable: PropTypes.bool, // optional - default false
isNilable: PropTypes.bool, // optional - default false
isExact: PropTypes.oneOfType([ // optional - default false
PropTypes.bool,
PropTypes.string,
PropTypes.any.custom(({prop}) => _.isError(prop) || _.isFunction(prop)),
]),
blacklist: PropTypes.arrayOf(PropTypes.string), // default null
blacklistError: PropTypes.oneOfType([ // default null
PropTypes.string,
PropTypes.any.custom(({prop}) => _.isError(prop) || _.isFunction(prop)),
]),
custom: PropTypes.function, // default null
objectNullError: PropTypes.oneOfType([ // default null
PropTypes.string,
PropTypes.any.custom(({prop}) => _.isError(prop) || _.isFunction(prop)),
]),
objectUndefinedError: PropTypes.oneOfType([ // default null
PropTypes.string,
PropTypes.any.custom(({prop}) => _.isError(prop) || _.isFunction(prop)),
]),
overrideError: PropTypes.oneOfType([
PropTypes.string,
PropTypes.any.custom(({prop}) => _.isError(prop) || _.isFunction(prop)),
]),
defaultError: PropTypes.oneOfType([
PropTypes.string,
PropTypes.any.custom(({prop}) => _.isError(prop) || _.isFunction(prop)),
]),
};
isNullable
the entire props object can benull
(but notundefined
)isNilable
the entire props object can be bothnull
andundefined
isExact
the entire props object cannot have unaccounted/extra props (if truthy must either be null, a string, an error object, or a function that returns or throws an error or string).blacklist
an array of propNames that are never allowed to be populatedblacklistError
when a custom blacklist error is needed, this option takes a string, error, or error-generating function (that gets checked at runtime) which gets used when the blacklist option is brokencustom
is a global custom function that is simply not attached to any prop at all (for convenience)objectNullError
: when the primary object to be checked is null (and not allowed to be null), this option takes a string, error, or error-generating function (that gets checked at runtime) which gets used instead of default messageobjectUndefinedError
: when the primary object to be checked is undefined (and not allowed to be undefined), this option takes a string, error, or error-generating function (that gets checked at runtime) which gets used instead of default messageoverrideError
: overrides any other error or message encountered. This option takes a string, error, or error-generating function (that gets checked at runtime) which gets used instead of default messagedefaultError
: overrides any check messages that are NOT already errors. This option takes a string, error, or error-generating function (that gets checked at runtime) which gets used instead of default message
Note that blacklist
and isExact
cannot be used simultaneously (error gets thrown).
If isNullable
or isNilable
is not used, the default case is that the object being checked must be, at a minimum, an object (or pass the _.isObject(obj)
test).
When a custom
function is included it will be called with {prop, props, propName}
, where prop
is simply the full object being checked, props === null
, and propName
has been hardcoded to be "global custom validation"
. This keeps the usage of custom
functions uniform throughout. In contrast to 'PropTypes.custom' as described below, the prop
parameter is always just undefined
.
Note that all custom
validation functions are deemed to PASS if it returns one of [true, null, undefined]
and to FAIL if it returns one of [false, <string>, <error>]
. All other return values will return a special validation error if any non-allowed return value is encountered.
propTypes arg
The propTypes object works the same as React prop-types (except for custom
validation function). Available propTypes include:
The simple ones:
any
a wildcard placeholder (althoughisRequired
andisRequiredOrNull
are enforced)string
bool
number
symbol
func
object
array
The functional ones:
oneOf
- takes an array of (at least 1) unique values that form an enum (not restricted to strings)instanceOf
- takes a single class (e.g. Date)arrayOf
- takes a propType (e.g.PropTypes.string
)objectOf
- takes a propType (e.g.PropTypes.string
)oneOfType
- takes an array of (at least 1) propTypes (e.g.[PropTypes.any]
)shape
- takes a propTypes object (recursive objects are possible)validator
- takes a previously defined validateProps(...) function
const funcPropTypes = {
derp: PropTypes.oneOf('asdf', 23, {}),
merp: PropTypes.instanceOf(Date),
lerp: PropTypes.arrayOf(PropTypes.string),
nerp: PropTypes.objectOf(PropTypes.string),
oneOfType: PropTypes.oneOfType([
PropTypes.string, // as an example
PropTypes.number,
]),
shape: PropTypes.shape({
PropTypes.string,
PropTypes.number,
}),
serp: PropTypes.validator(validate),
};
The shape
propType allows an option object, which only admits an {isExact: <bool>}
value. If {isExact: true}
is used, the props being checked is not allowed to have unaccounted/extra props.
The custom
type (which is treated special):
custom
takes a function which will be sent{prop, propName, props}
as single argument. Note thatcustom
propType cannot be further chained.
The default behavior is that, for all types, both null
and undefined
do not trigger a validation error. However, apart from custom
, both the simple propTypes and the functional propTypes can be extended with
isRequired
- neithernull
norundefined
are allowedisRequiredOrNull
-null
is allowed, but notundefined
custom
- same as the standalone, but this allows for easier separation of validation logic
Using an exmple from the simple group
const otherPropTypes = {
derp: PropTypes.string,
derpR: PropTypes.string.isRequired,
derpRN: PropTypes.string.isRequiredOrNull,
derpC: PropTypes.string.custom(({prop, propName, props}) => {...}),
derpRC: PropTypes.string.isRequired.custom(({prop, propName, props}) => {...}),
derpRNC: PropTypes.string.isRequiredOrNull.custom(({prop, propName, props}) => {...}),
};
const complicatedPropTypes = {
derp: PropTypes.shape({
merp: PropTypes.oneOf([
// note that in the following, all of derp is sent in as "props"
PropTypes.string.isRequiredOrNull.custom(({prop, propName, props}) => {...}),
PropTypes.number.isRequired,
]),
hasShapeWithOption: PropTypes.shape({arp: PropTypes.any}, {isExact: true}),
}).isRequiredOrNull.custom(({prop, propName, props}) => {...}),
},
errors vs. error messages in multi-layered objects
In cases with multiple levels (e.g.
const validate = validateProps({
top: PropTypes.shape({
a: PropTypes.string.custom(({prop}) => {
return 'short message',
}),
b: PropTypes.string.custom(({prop}) => {
return new Error('short message'),
}),
c: PropTypes.string.custom(({prop}) => {
throw new Error('short message'),
}),
}),
});
In the example above, props top.b
and top.c
will behave exactly the same, and the actual error created in the custom function is the one that will be thrown by the overall validate function. Likewise, when using check instead of validate, the message returned will be simply that of the error (i.e. "short message").
However, for the case top.a
above, the message will accumulate an address going upwards through the props chain, finally throwing an error with a such as
PropType validation error at [top, a]: Prop must be a string when included
custom errors
it is possible to inject custom errors at any level, which is the error that will be returned from the validator or checker. The error extension is generally available and overrides ANY error object accumulated so far (unless {isWeak: true}
option is used).
const arp4Error = new Error('arp4 error');
const arp5Error = new Error('arp5 error');
const types = {
arp1: PropTypes.string,
arp2: PropTypes.string.custom('arp2 error'),
arp3: PropTypes.string.custom(() => 'arp3 error'),
arp4: PropTypes.string.custom(arp4Error),
arp5: PropTypes.string.custom(() => arp5Error),
};
const checkArps = checkProps(types);
const validateArps = validateProps(types);
const mess1 = check({arp1: 23}); // "PropType validation error at [arp1]: Prop must be a string when included"
const mess2 = check({arp2: 23}); // "arp2 error"
const mess3 = check({arp3: 23}); // "arp3 error"
const mess4 = check({arp4: 23}); // "arp4 error"
const mess5 = check({arp5: 23}); // "arp5 error"
try {
validateArps({arp4: 23});
} catch (err) {
const isTheSame = (err === arp4Error); // true
}
try {
validateArps({arp5: 23});
} catch (err) {
const isTheSame = (err === arp45rror); // true
}
Note that the error extension never does anything unless some previous type checking logic has already blown up. Also note that this error WILL override any previously accrued errors\
Custom error options
When using validateProps as a way to map Error objects to different validation errors, it is convenient to be able to override any error message as long as it hasn't already been explicitly "cast" as an error, e.g.:
const morpTypes = {
morp1: PropTypes.shape({
snorp: PropTypes.string,
knorp: PropTypes.string.error('knorp custom error'),
}).error('injected custom error if an error not already being propagated up', {isWeak: true}),
morp2: PropTypes.shape({
snorp: PropTypes.string,
knorp: PropTypes.string.error('knorp custom error'),
}).error('always overrides every error'),
};
const checkMorps = checkProps(morpTypes);
const propsCases = [
{props: {}, mess: null},
{props: {morp1: {}, morp2: {}}, mess: null},
{props: {morp1: {snorp: 'a', knorp: 'b'}, morp2: {snorp: 'c', knorp: 'c'}}, mess: null},
{props: {morp1: {snorp: 23}}, mess: 'injected custom error if an error not already being propagated up'},
{props: {morp1: {knorp: 23}}, mess: 'knorp custom error'},
{props: {morp2: {snorp: 23}}, mess: 'always overrides every error'},
{props: {morp2: {knorp: 23}}, mess: 'always overrides every error'},
];
_.each(propsCases, propsCase => {
const mess = checkMorps(propsCase.props);
assert.strictEqual(mess, propsCase.mess);
});
Some other additions:
Capacity to NOT have to use PropTypes in an object. So it IS valid to write e.g.
const validateCustomString = validateProps(PropTypes.string.isRequired.custom(({prop}) => {
if (prop.length < 10) {
throw new Error('Invalid Custom String');
}
}));
License
Apache-2.0