feat: runtime prop validation

This commit is contained in:
Evan You 2018-10-02 18:29:14 -04:00
parent e93e85bb29
commit 2241ad7765
3 changed files with 129 additions and 13 deletions

View File

@ -29,7 +29,7 @@ export type PropType<T> = Prop<T> | Prop<T>[]
export type PropValidator<T> = PropOptions<T> | PropType<T>
export interface PropOptions<T = any> {
type?: PropType<T>
type?: PropType<T> | true | null
required?: boolean
default?: T | null | undefined | (() => T | null | undefined)
validator?(value: T): boolean

View File

@ -8,12 +8,11 @@ import { immutable, unwrap, lock, unlock } from '@vue/observer'
import {
Data,
ComponentPropsOptions,
PropValidator,
PropOptions,
Prop,
PropType
} from './componentOptions'
import { camelize, hyphenate } from './utils'
import { camelize, hyphenate, capitalize } from './utils'
export function initializeProps(instance: MountedComponent, data: Data | null) {
const { props, attrs } = resolveProps(
@ -136,7 +135,7 @@ export function resolveProps(
}
// runtime validation
if (__DEV__) {
validateProp(key, rawData[key], opt, Component)
validateProp(key, rawData[key], opt, Component, isAbsent)
}
}
}
@ -211,7 +210,7 @@ function isSameType(a: Prop<any>, b: Prop<any>): boolean {
function getTypeIndex(
type: Prop<any>,
expectedTypes: PropType<any> | void
expectedTypes: PropType<any> | void | null | true
): number {
if (Array.isArray(expectedTypes)) {
for (let i = 0, len = expectedTypes.length; i < len; i++) {
@ -219,17 +218,130 @@ function getTypeIndex(
return i
}
}
} else if (expectedTypes != null) {
} else if (expectedTypes != null && typeof expectedTypes === 'object') {
return isSameType(expectedTypes, type) ? 0 : -1
}
return -1
}
function validateProp(
key: string,
value: any,
validator: PropValidator<any>,
Component: ComponentClass | FunctionalComponent
) {
// TODO
type AssertionResult = {
valid: boolean
expectedType: string
}
function validateProp(
name: string,
value: any,
prop: PropOptions<any>,
Component: ComponentClass | FunctionalComponent,
isAbsent: boolean
) {
const { type, required, validator } = prop
// required!
if (required && isAbsent) {
console.warn('Missing required prop: "' + name + '"')
return
}
// missing but optional
if (value == null && !prop.required) {
return
}
// type check
if (type != null && type !== true) {
let isValid = false
const types = Array.isArray(type) ? type : [type]
const expectedTypes = []
// value is valid as long as one of the specified types match
for (let i = 0; i < types.length && !isValid; i++) {
const { valid, expectedType } = assertType(value, types[i])
expectedTypes.push(expectedType || '')
isValid = valid
}
if (!isValid) {
console.warn(getInvalidTypeMessage(name, value, expectedTypes))
return
}
}
// custom validator
if (validator && !validator(value)) {
console.warn(
'Invalid prop: custom validator check failed for prop "' + name + '".'
)
}
}
const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/
function assertType(value: any, type: Prop<any>): AssertionResult {
let valid
const expectedType = getType(type)
if (simpleCheckRE.test(expectedType)) {
const t = typeof value
valid = t === expectedType.toLowerCase()
// for primitive wrapper objects
if (!valid && t === 'object') {
valid = value instanceof type
}
} else if (expectedType === 'Object') {
valid = toRawType(value) === 'Object'
} else if (expectedType === 'Array') {
valid = Array.isArray(value)
} else {
valid = value instanceof type
}
return {
valid,
expectedType
}
}
function getInvalidTypeMessage(
name: string,
value: any,
expectedTypes: string[]
): string {
let message =
`Invalid prop: type check failed for prop "${name}".` +
` Expected ${expectedTypes.map(capitalize).join(', ')}`
const expectedType = expectedTypes[0]
const receivedType = toRawType(value)
const expectedValue = styleValue(value, expectedType)
const receivedValue = styleValue(value, receivedType)
// check if we need to specify expected value
if (
expectedTypes.length === 1 &&
isExplicable(expectedType) &&
!isBoolean(expectedType, receivedType)
) {
message += ` with value ${expectedValue}`
}
message += `, got ${receivedType} `
// check if we need to specify received value
if (isExplicable(receivedType)) {
message += `with value ${receivedValue}.`
}
return message
}
function styleValue(value: any, type: string): string {
if (type === 'String') {
return `"${value}"`
} else if (type === 'Number') {
return `${Number(value)}`
} else {
return `${value}`
}
}
function toRawType(value: any): string {
return Object.prototype.toString.call(value).slice(8, -1)
}
function isExplicable(type: string): boolean {
const explicitTypes = ['string', 'number', 'boolean']
return explicitTypes.some(elem => type.toLowerCase() === elem)
}
function isBoolean(...args: string[]): boolean {
return args.some(elem => elem.toLowerCase() === 'boolean')
}

View File

@ -55,6 +55,10 @@ export const hyphenate = (str: string): string => {
return str.replace(hyphenateRE, '-$1').toLowerCase()
}
export const capitalize = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1)
}
// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
export function lis(arr: number[]): number[] {
const p = arr.slice()