feat: runtime prop validation
This commit is contained in:
parent
e93e85bb29
commit
2241ad7765
@ -29,7 +29,7 @@ export type PropType<T> = Prop<T> | Prop<T>[]
|
|||||||
export type PropValidator<T> = PropOptions<T> | PropType<T>
|
export type PropValidator<T> = PropOptions<T> | PropType<T>
|
||||||
|
|
||||||
export interface PropOptions<T = any> {
|
export interface PropOptions<T = any> {
|
||||||
type?: PropType<T>
|
type?: PropType<T> | true | null
|
||||||
required?: boolean
|
required?: boolean
|
||||||
default?: T | null | undefined | (() => T | null | undefined)
|
default?: T | null | undefined | (() => T | null | undefined)
|
||||||
validator?(value: T): boolean
|
validator?(value: T): boolean
|
||||||
|
@ -8,12 +8,11 @@ import { immutable, unwrap, lock, unlock } from '@vue/observer'
|
|||||||
import {
|
import {
|
||||||
Data,
|
Data,
|
||||||
ComponentPropsOptions,
|
ComponentPropsOptions,
|
||||||
PropValidator,
|
|
||||||
PropOptions,
|
PropOptions,
|
||||||
Prop,
|
Prop,
|
||||||
PropType
|
PropType
|
||||||
} from './componentOptions'
|
} from './componentOptions'
|
||||||
import { camelize, hyphenate } from './utils'
|
import { camelize, hyphenate, capitalize } from './utils'
|
||||||
|
|
||||||
export function initializeProps(instance: MountedComponent, data: Data | null) {
|
export function initializeProps(instance: MountedComponent, data: Data | null) {
|
||||||
const { props, attrs } = resolveProps(
|
const { props, attrs } = resolveProps(
|
||||||
@ -136,7 +135,7 @@ export function resolveProps(
|
|||||||
}
|
}
|
||||||
// runtime validation
|
// runtime validation
|
||||||
if (__DEV__) {
|
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(
|
function getTypeIndex(
|
||||||
type: Prop<any>,
|
type: Prop<any>,
|
||||||
expectedTypes: PropType<any> | void
|
expectedTypes: PropType<any> | void | null | true
|
||||||
): number {
|
): number {
|
||||||
if (Array.isArray(expectedTypes)) {
|
if (Array.isArray(expectedTypes)) {
|
||||||
for (let i = 0, len = expectedTypes.length; i < len; i++) {
|
for (let i = 0, len = expectedTypes.length; i < len; i++) {
|
||||||
@ -219,17 +218,130 @@ function getTypeIndex(
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (expectedTypes != null) {
|
} else if (expectedTypes != null && typeof expectedTypes === 'object') {
|
||||||
return isSameType(expectedTypes, type) ? 0 : -1
|
return isSameType(expectedTypes, type) ? 0 : -1
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateProp(
|
type AssertionResult = {
|
||||||
key: string,
|
valid: boolean
|
||||||
value: any,
|
expectedType: string
|
||||||
validator: PropValidator<any>,
|
}
|
||||||
Component: ComponentClass | FunctionalComponent
|
|
||||||
) {
|
function validateProp(
|
||||||
// TODO
|
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')
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,10 @@ export const hyphenate = (str: string): string => {
|
|||||||
return str.replace(hyphenateRE, '-$1').toLowerCase()
|
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
|
// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
|
||||||
export function lis(arr: number[]): number[] {
|
export function lis(arr: number[]): number[] {
|
||||||
const p = arr.slice()
|
const p = arr.slice()
|
||||||
|
Loading…
Reference in New Issue
Block a user