docs: function typecast(value, originalValue, properties) {
          const options = this[_privateKey]._options;
  
          // Allow transform to manipulate raw properties.
          if (properties.transform) {
              value = properties.transform.call(this[_privateKey]._root, value, originalValue, properties);
          }
  
          // Allow null to be preserved.
          if (value === null && options.preserveNull) {
              return null;
          }
  
          // Helper function designed to detect and handle usage of array-form custom error messages for validators
          function detectCustomErrorMessage(key) {
              if (typeof properties[key] === 'object' && properties[key].errorMessage && properties[key].value) {
                  return properties[key];
              }
              else  if (_.isArray(properties[key])) {
                  return {
                      value: properties[key][0],
                      errorMessage: properties[key][1]
                  };
              }
              else {
                  return {
                      value: properties[key],
                      errorMessage: undefined
                  };
              }
          }
  
          // Property types are always normalized as lowercase strings despite shorthand definitions being available.
          switch (properties.type) {
              case 'string':
                  // Reject if object or array.
                  if (_.isObject(value) || _.isArray(value)) {
                      throw new StringCastError(value, originalValue, properties);
                  }
  
                  // If index is being set with null or undefined, set value and end.
                  if (value === undefined || value === null) {
                      return undefined;
                  }
  
                  // Typecast to String.
                  value = value + '';
  
                  // If stringTransform function is defined, use.
                  // This is used before we do validation checks (except to be sure we have a string at all).
                  if (properties.stringTransform) {
                      value = properties.stringTransform.call(this[_privateKey]._root, value, originalValue, properties);
                  }
  
                  // If clip property & maxLength properties are set, the string should be clipped.
                  // This is basically a shortcut property that could be done with stringTransform.
                  if (properties.clip !== undefined && properties.maxLength !== undefined) {
                      value = value.substr(0, properties.maxLength);
                  }
  
                  let enumValidation;
  
                  // Detect custom error message usage for enum (can't use function here as enum is expected to be an array)
                  if (typeof properties.enum === 'object' && properties.enum.errorMessage && properties.enum.value) {
                      enumValidation = properties.enum;
                  }
                  else if (_.isArray(properties.enum) && _.isArray(properties.enum[0])) {
                      enumValidation = {
                          value: properties.enum[0],
                          errorMessage: properties.enum[1]
                      };
                  }
                  else {
                      enumValidation = {
                          value: properties.enum,
                          errorMessage: undefined
                      };
                  }
  
                  // If enum is being used, be sure the value is within definition.
                  if (
                      enumValidation.value !== undefined &&
                      _.isArray(enumValidation.value) &&
                      enumValidation.value.indexOf(value) === -1
                  ) {
                      throw new StringEnumValidationError(enumValidation.errorMessage, value, originalValue, properties);
                  }
  
                  // Detect custom error message usage for minLength
                  let minLength = detectCustomErrorMessage('minLength');
  
                  // If minLength is defined, check to be sure the string is > minLength.
                  if (minLength.value !== undefined && value.length < minLength.value) {
                      throw new StringMinLengthValidationError(minLength.errorMessage, value, originalValue, properties);
                  }
  
                  // Detect custom error message usage for maxLength
                  let maxLength = detectCustomErrorMessage('maxLength');
  
                  // If maxLength is defined, check to be sure the string is < maxLength.
                  if (maxLength.value !== undefined && value.length > maxLength.value) {
                      throw new StringMaxLengthValidationError(maxLength.errorMessage, value, originalValue, properties);
                  }
  
                  // Detect custom error message usage for maxLength
                  let regex = detectCustomErrorMessage('regex');
  
                  // If regex is defined, check to be sure the string matches the regex pattern.
                  if (regex.value && !regex.value.test(value)) {
                      throw new StringRegexValidationError(regex.errorMessage, value, originalValue, properties);
                  }
  
                  return value;
  
              case 'number':
                  // If index is being set with null, undefined, or empty string: clear value.
                  if (value === undefined || value === null || value === '') {
                      return undefined;
                  }
  
                  // Set values for boolean.
                  if (_.isBoolean(value)) {
                      value = value ? 1 : 0;
                  }
  
                  // Remove/convert number group separators
                  if (typeof value === 'string') {
                      if (options.useDecimalNumberGroupSeparator) {
                          // Remove decimals
                          value = value.replace(/\./g, '');
                          // Replace commas with decimals for js parsing
                          value = value.replace(/,/g, '.');
                      }
                      else {
                          //Remove commas
                          value = value.replace(/,/g, '');
                      }
  
                      // Reject if string was not a valid number
                      if (isNaN(Number(value))) {
                        throw new NumberCastError('String', value, originalValue, properties);
                      }
                  }
  
                  // Reject if array, object, or not numeric.
                  if (_.isArray(value)) {
                      throw new NumberCastError('Array', value, originalValue, properties);
                  }
                  else if (_.isObject(value)) {
                      throw new NumberCastError('Object', value, originalValue, properties);
                  }
                  else if (!isNumeric(value)) {
                      throw new NumberCastError('Non-numeric', value, originalValue, properties);
                  }
  
                  // Typecast to number.
                  value = Number(value);
  
                  // Transformation after typecasting but before validation and filters.
                  if (properties.numberTransform) {
                      value = properties.numberTransform.call(this[_privateKey]._root, value, originalValue, properties);
                  }
  
                  // Detect custom error message usage for min
                  let min = detectCustomErrorMessage('min');
  
                  if (min.value !== undefined && value < min.value) {
                      throw new NumberMinValidationError(min.errorMessage, value, originalValue, properties);
                  }
  
                  // Detect custom error message usage for min
                  let max = detectCustomErrorMessage('max');
  
                  if (max.value !== undefined && value > max.value) {
                      throw new NumberMaxValidationError(max.errorMessage, value, originalValue, properties);
                  }
  
                  return value;
  
              case 'boolean':
                  // If index is being set with null, undefined, or empty string: clear value.
                  if (value === undefined || value === null || value === '') {
                      return undefined;
                  }
  
                  // If is String and is 'false', convert to Boolean.
                  if (value === 'false') {
                      return false;
                  }
  
                  // If is Number, <0 is true and >0 is false.
                  if (isNumeric(value)) {
                      return (value * 1) > 0;
                  }
  
                  // Use Javascript to eval and return boolean.
                  value = !!value;
  
                  // Transformation after typecasting but before validation and filters.
                  if (properties.booleanTransform) {
                      value = properties.booleanTransform.call(this[_privateKey]._root, value, originalValue, properties);
                  }
  
                  return value;
  
              case 'array':
                  // If it's an object, typecast to an array and return array.
                  if (_.isObject(value)) {
                      value = _.toArray(value);
                  }
  
                  // Reject if not array.
                  if (!_.isArray(value)) {
                      throw new ArrayCastError(value, originalValue, properties);
                  }
  
                  // Arrays are never set directly.
                  // Instead, the values are copied over to the existing SchemaArray instance.
                  // The SchemaArray is initialized immediately and will always exist.
                  originalValue.length = 0;
                  _.each(value, (arrayValue) => {
                      originalValue.push(arrayValue);
                  });
  
                  return originalValue;
  
              case 'object':
                  // If it's not an Object, reject.
                  if (!_.isObject(value)) {
                      throw new ObjectCastError(value, originalValue, properties);
                  }
  
                  // If object is schema object and an entirely new object was passed, clear values and set.
                  // This preserves the object instance.
                  if (properties.objectType) {
                      // The object will usually exist because it's initialized immediately for deep access within SchemaObjects.
                      // However, in the case of Array elements, it will not exist.
                      let schemaObject;
                      if (originalValue !== undefined) {
                          // Clear existing values.
                          schemaObject = originalValue;
                          schemaObject.clear();
                      } else {
                          // The SchemaObject doesn't exist yet. Let's initialize a new one.
                          // This is used for Array types.
                          schemaObject = new properties.objectType({}, this[_privateKey]._root);
                      }
  
                      // Copy value to SchemaObject and set value to SchemaObject.
                      for (const key in value) {
                          schemaObject[key] = value[key];
                      }
                      value = schemaObject;
                  }
  
                  // Otherwise, it's OK.
                  return value;
  
              case 'date':
                  // If index is being set with null, undefined, or empty string: clear value.
                  if (value === undefined || value === null || value === '') {
                      return undefined;
                  }
  
                  // Reject if object, array or boolean.
                  if (!_.isDate(value) && !_.isString(value) && !_.isNumber(value)) {
                      throw new DateCastError(value, originalValue, properties);
                  }
  
                  // Attempt to parse string value with Date.parse (which returns number of milliseconds).
                  if (_.isString(value)) {
                      value = Date.parse(value);
                  }
  
                  // If is timestamp, convert to Date.
                  if (isNumeric(value)) {
                      value = new Date((value + '').length > 10 ? value : value * 1000);
                  }
  
                  // If the date couldn't be parsed, do not modify index.
                  if (value == 'Invalid Date' || !_.isDate(value)) {
                      throw new DateParseValidationError(null, value, originalValue, properties);
                  }
  
                  // Transformation after typecasting but before validation and filters.
                  if (properties.dateTransform) {
                      value = properties.dateTransform.call(this[_privateKey]._root, value, originalValue, properties);
                  }
  
                  return value;
  
              default: // 'any'
                  return value;
          }
      }