如何创建元组道具类型检查器?

时间:2016-09-14 18:37:00

标签: validation reactjs

我正在尝试为React创建一个自定义的“元组”道具类型,但是,我遇到了一些障碍。

API看起来像这样:

import {PropTypes} from 'react';

export default class MyComponent extends React.Component {

    static propTypes = {
        myProp: tuple(PropTypes.number, PropTypes.string),
    };

这将确保myProp是一个2元素数组,第一个元素是数字,第二个元素是字符串。

这是我走了多远:

export function tuple(...arrayOfTypeCheckers) {
    return requirable(function(props, propName, componentName) {
        let value = props[propName];
        if(!Array.isArray(value)) {
            throw new Error(`Expected array for \`${propName}\` in \`${componentName}\``);
        }
        if(value.length !== arrayOfTypeCheckers.length) {
            throw new Error(`\`${propName}\` must have exactly ${arrayOfTypeCheckers.length} elements in \`${componentName}\`, got ${value.length}`);
        }
        for(let i = 0; i < value.length; ++i) {
            let checker = arrayOfTypeCheckers[i];

            if(!__NEED_HELP_HERE__) {
                throw new Error(`${propName}[${i}] is not of the expected type in \`${componentName}\``)
            }
        }

        return null;
    });
}

requirable的实施可以找到here

但是,我无法弄清楚如何实施__NEED_HELP_HERE__PropTypes.string是带签名的另一个道具类型

function(props, propName, componentName)

你会注意到它需要一个装满道具的物品,还有一个propName。该值被访问为

var propValue = props[propName];

这意味着我无法从数组中提供自己的值。

如何调用React的PropTypes.xyz验证器?

3 个答案:

答案 0 :(得分:2)

我发现这个问题很有用,所以我想为任何正在寻找这个问题的人(即使它已经使用了两年)提供一个最新的答案。答案基于@mpen的答案。已更新,以与最新的prop-types版本15.6.2一起使用。

主要更新是将secret(类型检查器的隐藏第6个参数)传递给内部类型检查器。这样就可以在此检查器中使用标准prop-types类型的检查器,而不会抛出Error。也可以在其他类型检查器中使用(shapearrayOfobjectOftuple本身等等)

该实现也基于prop-types的内部实现,并符合其API(返回Errornull)。

createChainableTypeChecker的实现是从库中取出的,其中删除了一些内容(检查secret是否正确),并添加了secret向下传递。

这是解决方案。它使用ES6类和模块导入/导出:

function CustomPropTypeError (message) {
  this.message = message;
  this.stack = '';
}
CustomPropTypeError.prototype = Error.prototype;

function createChainableTypeChecker (validate) {
  function checkType (isRequired, props, propName, componentName, location, propFullName, secret) {
    componentName = componentName || ANONYMOUS;
    propFullName = propFullName || propName;

    if (props[propName] == null) {
      if (isRequired) {
        if (props[propName] === null) {
          return new CustomPropTypeError('The ' + location + ' `' + propFullName + '` is marked as required ' + ('in `' + componentName + '`, but its value is `null`.'));
        }
        return new CustomPropTypeError('The ' + location + ' `' + propFullName + '` is marked as required in ' + ('`' + componentName + '`, but its value is `undefined`.'));
      }
      return null;
    } else {
      return validate(props, propName, componentName, location, propFullName, secret);
    }
  }

  var chainedCheckType = checkType.bind(null, false);
  chainedCheckType.isRequired = checkType.bind(null, true);

  return chainedCheckType;
}

export function tuple (...types) {
  return createChainableTypeChecker((props, propName, componentName, location, propFullName, secret) => {
    const value = props[propName];
    if (!location) {
      location = 'prop';
    }
    if (!propFullName) {
      propFullName = propName;
    }

    if (!Array.isArray(value)) {
      return new CustomPropTypeError(`Invalid ${location} \`${propFullName}\` supplied to \`${componentName}\`, expected ${types.length}-element array`);
    }
    if (value.length !== types.length) {
      return new CustomPropTypeError(`Invalid ${location} \`${propFullName}\` supplied to \`${componentName}\`, expected ${types.length}-element array, got array of length ${value.length}`);
    }
    for (let i = 0; i < value.length; ++i) {
      const error = types[i](value, i, componentName, 'element', `${propFullName}[${i}]`, secret);
      if (error) {
        return error;
      }
    }

    return null;
  });
}

用法:

import React from 'react';
import PropTypes from 'prop-types';
import * as CustomPropTypes from './path/to/CustomPropTypes';

class Component extends React.Component { ... }
Component.propTypes = {
  myTupleType: CustomPropTypes.tuple(
    PropTypes.number.isRequired,
    PropTypes.string.isRequired
  ).isRequired,
};

<Component myTupleType={[5, 'string']} />  // No error
<Component myTupleType={['5', 6]} />       // PropType error

注意事项:

  • prop-types不鼓励创建使用包含的类型检查器的自定义类型检查器(出于某种原因)。这似乎可以在15.6.2中使用,但是我不能保证它可以在其他版本中使用,或者在以后的版本中仍然可以使用。
  • 我所做的测试是有限的(但对我来说没有错误)。边缘情况可能会导致错误(prop-types进行了其他尝试以防止扩展)。

答案 1 :(得分:0)

使用React的内部API太难了,他们反正告诉我们not to,所以我必须自己重新实现类型检查器。< / p>

以下是代码:

const lo = require('lodash');

function tuple(...types) {
    return requirable(function(props, propName, componentName, location, propFullName) {
        let value = props[propName];
        if(!location) {
            location = 'prop';
        }
        if(!propFullName) {
            propFullName = propName;
        }

        if(!Array.isArray(value)) {
            throw new Error(`Invalid ${location} \`${propFullName}\` supplied to \`${componentName}\`, expected ${types.length}-element array`);
        }
        if(value.length !== types.length) {
            throw new Error(`Invalid ${location} \`${propFullName}\` supplied to \`${componentName}\`, expected ${types.length}-element array, got array of length ${value.length}`);
        }
        for(let i = 0; i < value.length; ++i) {
            if(!types[i](value[i])) {
                throw new Error(`Invalid ${location} ${propFullName}[${i}] supplied to \`${componentName}\`, unexpected type`)
            }
        }

        return null;
    });
}

function requirable(predicate) {
    const propType = (props, propName, ...rest) => {
        // don't do any validation if empty
        if(props[propName] === undefined) {
            return;
        }

        return predicate(props, propName, ...rest);
    };

    propType.isRequired = (props, propName, componentName, ...rest) => {
        // warn if empty
        if(props[propName] === undefined) {
            return new Error(`Required prop \`${propName}\` was not specified in \`${componentName}\`.`);
        }

        return predicate(props, propName, componentName, ...rest);
    };

    return propType;
}

const TypeCheckers = {
    TypedArray: lo.isTypedArray,
    Object: lo.isObject,
    PlainObject: lo.isPlainObject,
    RegExp: lo.isRegExp,
    String: lo.isString,
    Undefined: lo.isUndefined,
    Number: lo.isNumber,
    Null: lo.isNull,
    NativeFunction: lo.isNative,
    Function: lo.isFunction,
    Error: lo.isError,
    FiniteNumber: lo.isFinite,
    NaN: lo.isNaN,
    DomElement: lo.isElement,
    Date: lo.isDate,
    Array: lo.isArray,
    Boolean: lo.isBoolean,
    Stringable: obj => obj && lo.isFunction(obj.toString),
};

&#34;类型&#34;不是实际类型,它们只是取值并返回真或假的函数。我使用lodash中的一些辅助函数,因为这比试图将它们从React中删除要容易得多,但你可以使用任何东西。

仍在使用内部API来获取propFullName,但是如果他们删除了它,我会进行后备。没有完整的道具名称,错误消息看起来非常糟糕。 e.g。

  

警告:失败的道具类型:提供给PhsaDocToolbar的意外类型

的道具0 [1]无效

但是propFullName我们得到:

  

警告:失败道具类型:提供给PhsaDocToolbar的意外类型无效道具[0] [1]

只有您自己创建PropTypes.arrayOf(tuple(TypeCheckers.Number, TypeCheckers.String))等内容才能解决问题。

答案 2 :(得分:0)

做到了,对我来说很好。

//Component
<Component state={[10, () => 20]} />

//Tupple
const tupple = (...validators) =>
    PropTypes.arrayOf((_, index) => {
        const currentValidators = validators.filter((v, i) => i === index);
        if (currentValidators.length <= 0) return true;
        const [currentValidator] = currentValidators;

        return currentValidator;
    });

//PropsValidation
SomeComponent.propTypes = {
    state: tupple(
        PropTypes.number,
        PropTypes.func
    ),
};