vue3-core/packages/runtime-core/src/componentProps.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

863 lines
24 KiB
TypeScript
Raw Normal View History

import {
TriggerOpTypes,
shallowReactive,
shallowReadonly,
toRaw,
trigger,
} from '@vue/reactivity'
2019-05-28 18:06:00 +08:00
import {
EMPTY_ARR,
2019-05-28 18:06:00 +08:00
EMPTY_OBJ,
type IfAny,
PatchFlags,
2019-05-28 18:06:00 +08:00
camelize,
capitalize,
2021-05-01 03:50:32 +08:00
extend,
hasOwn,
2019-05-28 18:06:00 +08:00
hyphenate,
isArray,
2019-05-28 18:06:00 +08:00
isFunction,
2019-06-03 13:44:45 +08:00
isObject,
2022-01-16 15:43:19 +08:00
isOn,
isReservedProp,
2019-05-28 18:06:00 +08:00
isString,
makeMap,
toRawType,
2019-05-28 18:06:00 +08:00
} from '@vue/shared'
import { warn } from './warning'
import {
type ComponentInternalInstance,
type ComponentOptions,
type ConcreteComponent,
2024-12-04 13:50:54 +08:00
type Data,
type GenericComponentInstance,
setCurrentInstance,
} from './component'
import { isEmitListener } from './componentEmits'
import type { AppContext } from './apiCreateApp'
2021-04-06 23:08:21 +08:00
import { createPropsDefaultThis } from './compat/props'
import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig'
import { DeprecationTypes } from './compat/compatConfig'
2021-04-22 10:04:26 +08:00
import { shouldSkipAttr } from './compat/attrsFallthrough'
import { createInternalObject } from './internalObject'
2019-05-28 18:06:00 +08:00
2019-10-08 21:26:09 +08:00
export type ComponentPropsOptions<P = Data> =
| ComponentObjectPropsOptions<P>
| string[]
export type ComponentObjectPropsOptions<P = Data> = {
2019-06-01 00:47:05 +08:00
[K in keyof P]: Prop<P[K]> | null
2019-05-28 18:06:00 +08:00
}
export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
2019-05-28 18:06:00 +08:00
type DefaultFactory<T> = (props: Data) => T | null | undefined
2021-06-27 09:11:57 +08:00
export interface PropOptions<T = any, D = T> {
2019-05-28 18:06:00 +08:00
type?: PropType<T> | true | null
required?: boolean
default?: D | DefaultFactory<D> | null | undefined | object
validator?(value: unknown, props: Data): boolean
/**
* @internal
*/
skipCheck?: boolean
/**
* @internal
*/
skipFactory?: boolean
2019-05-31 18:07:43 +08:00
}
export type PropType<T> = PropConstructor<T> | (PropConstructor<T> | null)[]
2019-06-01 00:47:05 +08:00
type PropConstructor<T = any> =
| { new (...args: any[]): T & {} }
| { (): T }
| PropMethod<T>
type PropMethod<T, TConstructor = any> = [T] extends [
((...args: any) => any) | undefined,
] // if is function with args, allowing non-required functions
? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor
: never
2019-06-01 00:47:05 +08:00
type RequiredKeys<T> = {
[K in keyof T]: T[K] extends
| { required: true }
| { default: any }
// don't mark Boolean props as undefined
| BooleanConstructor
| { type: BooleanConstructor }
2021-07-20 06:24:18 +08:00
? T[K] extends { default: undefined | (() => undefined) }
? never
: K
: never
2019-06-01 00:47:05 +08:00
}[keyof T]
type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>
type DefaultKeys<T> = {
[K in keyof T]: T[K] extends
| { default: any }
// Boolean implicitly defaults to false
| BooleanConstructor
| { type: BooleanConstructor }
? T[K] extends { type: BooleanConstructor; required: true } // not default if Boolean is marked as required
? never
: K
: never
}[keyof T]
2019-06-01 00:47:05 +08:00
type InferPropType<T, NullAsAny = true> = [T] extends [null]
? NullAsAny extends true
? any
: null
: [T] extends [{ type: null | true }]
? any // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean`
: [T] extends [ObjectConstructor | { type: ObjectConstructor }]
? Record<string, any>
: [T] extends [BooleanConstructor | { type: BooleanConstructor }]
? boolean
: [T] extends [DateConstructor | { type: DateConstructor }]
? Date
: [T] extends [(infer U)[] | { type: (infer U)[] }]
? U extends DateConstructor
? Date | InferPropType<U, false>
: InferPropType<U, false>
: [T] extends [Prop<infer V, infer D>]
? unknown extends V
? keyof V extends never
? IfAny<V, V, D>
: V
: V
: T
2019-06-01 00:47:05 +08:00
/**
* Extract prop types from a runtime props options object.
* The extracted types are **internal** - i.e. the resolved props received by
* the component.
* - Boolean props are always present
* - Props with default values are always present
*
* To extract accepted props from the parent, use {@link ExtractPublicPropTypes}.
*/
export type ExtractPropTypes<O> = {
// use `keyof Pick<O, RequiredKeys<O>>` instead of `RequiredKeys<O>` to
// support IDE features
[K in keyof Pick<O, RequiredKeys<O>>]: InferPropType<O[K]>
} & {
// use `keyof Pick<O, OptionalKeys<O>>` instead of `OptionalKeys<O>` to
// support IDE features
[K in keyof Pick<O, OptionalKeys<O>>]?: InferPropType<O[K]>
}
2019-05-28 18:06:00 +08:00
type PublicRequiredKeys<T> = {
[K in keyof T]: T[K] extends { required: true } ? K : never
}[keyof T]
type PublicOptionalKeys<T> = Exclude<keyof T, PublicRequiredKeys<T>>
/**
* Extract prop types from a runtime props options object.
* The extracted types are **public** - i.e. the expected props that can be
* passed to component.
*/
export type ExtractPublicPropTypes<O> = {
[K in keyof Pick<O, PublicRequiredKeys<O>>]: InferPropType<O[K]>
} & {
[K in keyof Pick<O, PublicOptionalKeys<O>>]?: InferPropType<O[K]>
}
enum BooleanFlags {
2019-11-25 05:00:46 +08:00
shouldCast,
shouldCastTrue,
2019-05-28 18:06:00 +08:00
}
// extract props which defined with default from prop options
export type ExtractDefaultPropTypes<O> = O extends object
? // use `keyof Pick<O, DefaultKeys<O>>` instead of `DefaultKeys<O>` to support IDE features
{ [K in keyof Pick<O, DefaultKeys<O>>]: InferPropType<O[K]> }
: {}
type NormalizedProp = PropOptions & {
[BooleanFlags.shouldCast]?: boolean
[BooleanFlags.shouldCastTrue]?: boolean
}
2019-05-28 18:06:00 +08:00
/**
* normalized value is a tuple of the actual normalized options
* and an array of prop keys that need value casting (booleans and defaults)
*/
export type NormalizedProps = Record<string, NormalizedProp>
2025-02-03 15:46:40 +08:00
export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
2019-05-28 18:06:00 +08:00
export function initProps(
2019-09-07 00:58:31 +08:00
instance: ComponentInternalInstance,
rawProps: Data | null,
isStateful: number, // result of bitwise flag comparison
isSSR = false,
): void {
2024-12-10 08:11:36 +08:00
const props: Data = (instance.props = {})
const attrs: Data = createInternalObject()
instance.propsDefaults = Object.create(null)
setFullProps(instance, rawProps, props, attrs)
// ensure all declared prop keys are present
for (const key in instance.propsOptions[0]) {
if (!(key in props)) {
props[key] = undefined
}
}
// validation
if (__DEV__) {
2024-12-05 16:14:24 +08:00
validateProps(rawProps || {}, props, instance.propsOptions[0]!)
2019-05-28 18:06:00 +08:00
}
2019-05-30 23:16:15 +08:00
if (isStateful) {
// stateful
instance.props = isSSR ? props : shallowReactive(props)
} else {
if (!instance.type.props) {
// functional w/ optional props, props === attrs
instance.props = attrs
} else {
// functional w/ declared props
instance.props = props
}
}
instance.attrs = attrs
}
2019-05-30 23:16:15 +08:00
2025-02-04 22:44:17 +08:00
function isInHmrContext(instance: GenericComponentInstance | null) {
while (instance) {
if (instance.type.__hmrId) return true
instance = instance.parent
}
}
export function updateProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
rawPrevProps: Data | null,
optimized: boolean,
): void {
const {
props,
attrs,
vnode: { patchFlag },
} = instance
const rawCurrentProps = toRaw(props)
const [options] = instance.propsOptions
let hasAttrsChanged = false
if (
// always force full diff in dev
// - #1942 if hmr is enabled with sfc component
// - vite#872 non-sfc component used by sfc component
!(__DEV__ && isInHmrContext(instance)) &&
(optimized || patchFlag > 0) &&
!(patchFlag & PatchFlags.FULL_PROPS)
) {
if (patchFlag & PatchFlags.PROPS) {
// Compiler-generated props & no keys change, just set the updated
// the props.
const propsToUpdate = instance.vnode.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
2021-05-01 03:50:32 +08:00
let key = propsToUpdate[i]
// skip if the prop key is a declared emit event listener
if (isEmitListener(instance.emitsOptions, key)) {
continue
}
// PROPS flag guarantees rawProps to be non-null
const value = rawProps![key]
if (options) {
// attr / props separation was done on init and will be consistent
// in this code path, so just check if attrs have it.
if (hasOwn(attrs, key)) {
if (value !== attrs[key]) {
attrs[key] = value
hasAttrsChanged = true
}
} else {
const camelizedKey = camelize(key)
props[camelizedKey] = resolvePropValue(
options,
camelizedKey,
value,
instance,
2024-12-03 16:48:28 +08:00
baseResolveDefault,
)
}
} else {
2021-05-01 03:50:32 +08:00
if (__COMPAT__) {
if (isOn(key) && key.endsWith('Native')) {
key = key.slice(0, -6) // remove Native postfix
} else if (shouldSkipAttr(key, instance)) {
continue
}
2021-04-20 21:25:12 +08:00
}
if (value !== attrs[key]) {
attrs[key] = value
hasAttrsChanged = true
}
}
}
}
} else {
// full props update.
if (setFullProps(instance, rawProps, props, attrs)) {
hasAttrsChanged = true
}
// in case of dynamic props, check if we need to delete keys from
// the props object
let kebabKey: string
for (const key in rawCurrentProps) {
if (
!rawProps ||
// for camelCase
(!hasOwn(rawProps, key) &&
// it's possible the original props was passed in as kebab-case
// and converted to camelCase (#955)
((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))
) {
if (options) {
if (
rawPrevProps &&
// for camelCase
(rawPrevProps[key] !== undefined ||
// for kebab-case
rawPrevProps[kebabKey!] !== undefined)
) {
props[key] = resolvePropValue(
options,
key,
undefined,
instance,
2024-12-03 16:48:28 +08:00
baseResolveDefault,
true /* isAbsent */,
)
}
} else {
delete props[key]
}
}
}
// in the case of functional component w/o props declaration, props and
// attrs point to the same object so it should already have been updated.
if (attrs !== rawCurrentProps) {
for (const key in attrs) {
if (
!rawProps ||
(!hasOwn(rawProps, key) &&
(!__COMPAT__ || !hasOwn(rawProps, key + 'Native')))
) {
delete attrs[key]
hasAttrsChanged = true
}
}
}
}
// trigger updates for $attrs in case it's used in component slots
if (hasAttrsChanged) {
trigger(instance.attrs, TriggerOpTypes.SET, '')
}
if (__DEV__) {
2024-12-05 16:14:24 +08:00
validateProps(rawProps || {}, props, instance.propsOptions[0]!)
}
}
function setFullProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
props: Data,
attrs: Data,
) {
const [options, needCastKeys] = instance.propsOptions
let hasAttrsChanged = false
let rawCastValues: Data | undefined
2020-03-19 06:14:51 +08:00
if (rawProps) {
2021-05-01 03:50:32 +08:00
for (let key in rawProps) {
// key, ref are reserved and never passed down
if (isReservedProp(key)) {
continue
}
2021-04-18 11:19:40 +08:00
if (__COMPAT__) {
if (key.startsWith('onHook:')) {
softAssertCompatEnabled(
DeprecationTypes.INSTANCE_EVENT_HOOKS,
instance,
key.slice(2).toLowerCase(),
)
}
if (key === 'inline-template') {
continue
}
}
const value = rawProps[key]
// prop option names are camelized during normalization, so to support
// kebab -> camel conversion here we need to camelize the key.
let camelKey
if (options && hasOwn(options, (camelKey = camelize(key)))) {
if (!needCastKeys || !needCastKeys.includes(camelKey)) {
props[camelKey] = value
} else {
;(rawCastValues || (rawCastValues = {}))[camelKey] = value
}
} else if (!isEmitListener(instance.emitsOptions, key)) {
// Any non-declared (either as a prop or an emitted event) props are put
// into a separate `attrs` object for spreading. Make sure to preserve
// original key casing
2021-05-01 03:50:32 +08:00
if (__COMPAT__) {
if (isOn(key) && key.endsWith('Native')) {
key = key.slice(0, -6) // remove Native postfix
} else if (shouldSkipAttr(key, instance)) {
continue
}
2021-04-20 21:25:12 +08:00
}
if (!(key in attrs) || value !== attrs[key]) {
attrs[key] = value
hasAttrsChanged = true
}
2019-05-28 18:06:00 +08:00
}
}
}
if (needCastKeys) {
const castValues = rawCastValues || EMPTY_OBJ
for (let i = 0; i < needCastKeys.length; i++) {
const key = needCastKeys[i]
props[key] = resolvePropValue(
options!,
key,
castValues[key],
instance,
2024-12-03 16:48:28 +08:00
baseResolveDefault,
!hasOwn(castValues, key),
)
2019-05-28 18:06:00 +08:00
}
}
return hasAttrsChanged
}
2019-05-29 11:36:16 +08:00
2024-12-03 16:48:28 +08:00
/**
* @internal for runtime-vapor
*/
export function resolvePropValue<
T extends GenericComponentInstance & Pick<ComponentInternalInstance, 'ce'>,
>(
options: NormalizedProps,
key: string,
value: unknown,
2024-12-03 16:48:28 +08:00
instance: T,
/**
* Allow runtime-specific default resolution logic
*/
resolveDefault: (
factory: (props: Data) => unknown,
instance: T,
key: string,
) => unknown,
isAbsent = false,
): unknown {
const opt = options[key]
if (opt != null) {
const hasDefault = hasOwn(opt, 'default')
// default values
if (hasDefault && value === undefined) {
const defaultValue = opt.default
if (
opt.type !== Function &&
!opt.skipFactory &&
isFunction(defaultValue)
) {
2024-12-03 16:48:28 +08:00
const cachedDefaults =
instance.propsDefaults || (instance.propsDefaults = {})
if (hasOwn(cachedDefaults, key)) {
value = cachedDefaults[key]
} else {
2024-12-03 16:48:28 +08:00
value = cachedDefaults[key] = resolveDefault(
defaultValue,
instance,
key,
)
}
} else {
value = defaultValue
}
// #9006 reflect default value on custom element
if (instance.ce) {
instance.ce._setProp(key, value)
}
}
// boolean casting
if (opt[BooleanFlags.shouldCast]) {
if (isAbsent && !hasDefault) {
value = false
} else if (
opt[BooleanFlags.shouldCastTrue] &&
(value === '' || value === hyphenate(key))
) {
value = true
}
}
}
return value
}
2024-12-03 16:48:28 +08:00
/**
* runtime-dom-specific default resolving logic
*/
function baseResolveDefault(
factory: (props: Data) => unknown,
instance: ComponentInternalInstance,
key: string,
) {
let value
const reset = setCurrentInstance(instance)
const props = toRaw(instance.props)
value = factory.call(
__COMPAT__ && isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
? createPropsDefaultThis(instance, props, key)
: null,
props,
)
reset()
return value
}
const mixinPropsCache = new WeakMap<ConcreteComponent, NormalizedPropsOptions>()
export function normalizePropsOptions(
comp: ConcreteComponent,
appContext: AppContext,
asMixin = false,
): NormalizedPropsOptions {
const cache =
__FEATURE_OPTIONS_API__ && asMixin ? mixinPropsCache : appContext.propsCache
const cached = cache.get(comp)
if (cached) {
return cached
2019-05-28 18:06:00 +08:00
}
const raw = comp.props
const normalized: NormalizedPropsOptions[0] = {}
const needCastKeys: NormalizedPropsOptions[1] = []
// apply mixin/extends props
let hasExtends = false
if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
const extendProps = (raw: ComponentOptions) => {
2021-04-23 02:59:54 +08:00
if (__COMPAT__ && isFunction(raw)) {
raw = raw.options
}
hasExtends = true
const [props, keys] = normalizePropsOptions(raw, appContext, true)
extend(normalized, props)
if (keys) needCastKeys.push(...keys)
}
if (!asMixin && appContext.mixins.length) {
appContext.mixins.forEach(extendProps)
}
if (comp.extends) {
extendProps(comp.extends)
}
if (comp.mixins) {
comp.mixins.forEach(extendProps)
}
}
if (!raw && !hasExtends) {
if (isObject(comp)) {
cache.set(comp, EMPTY_ARR as any)
}
return EMPTY_ARR as any
}
2024-12-03 16:48:28 +08:00
baseNormalizePropsOptions(raw, normalized, needCastKeys)
const res: NormalizedPropsOptions = [normalized, needCastKeys]
if (isObject(comp)) {
cache.set(comp, res)
}
return res
}
/**
* @internal for runtime-vapor only
*/
export function baseNormalizePropsOptions(
raw: ComponentPropsOptions | undefined,
normalized: NonNullable<NormalizedPropsOptions[0]>,
needCastKeys: NonNullable<NormalizedPropsOptions[1]>,
): void {
2019-05-28 18:06:00 +08:00
if (isArray(raw)) {
for (let i = 0; i < raw.length; i++) {
if (__DEV__ && !isString(raw[i])) {
warn(`props must be strings when using array syntax.`, raw[i])
}
const normalizedKey = camelize(raw[i])
if (validatePropName(normalizedKey)) {
normalized[normalizedKey] = EMPTY_OBJ
2019-05-28 18:06:00 +08:00
}
}
} else if (raw) {
2019-05-28 18:06:00 +08:00
if (__DEV__ && !isObject(raw)) {
warn(`invalid props options`, raw)
}
for (const key in raw) {
const normalizedKey = camelize(key)
if (validatePropName(normalizedKey)) {
2019-05-28 18:06:00 +08:00
const opt = raw[key]
const prop: NormalizedProp = (normalized[normalizedKey] =
2023-02-03 09:54:15 +08:00
isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt))
const propType = prop.type
let shouldCast = false
let shouldCastTrue = true
if (isArray(propType)) {
for (let index = 0; index < propType.length; ++index) {
const type = propType[index]
const typeName = isFunction(type) && type.name
if (typeName === 'Boolean') {
shouldCast = true
break
} else if (typeName === 'String') {
// If we find `String` before `Boolean`, e.g. `[String, Boolean]`,
// we need to handle the casting slightly differently. Props
// passed as `<Comp checked="">` or `<Comp checked="checked">`
// will either be treated as strings or converted to a boolean
// `true`, depending on the order of the types.
shouldCastTrue = false
}
}
} else {
shouldCast = isFunction(propType) && propType.name === 'Boolean'
}
prop[BooleanFlags.shouldCast] = shouldCast
prop[BooleanFlags.shouldCastTrue] = shouldCastTrue
// if the prop needs boolean casting or default value
if (shouldCast || hasOwn(prop, 'default')) {
needCastKeys.push(normalizedKey)
2019-05-28 18:06:00 +08:00
}
}
}
}
}
function validatePropName(key: string) {
if (key[0] !== '$' && !isReservedProp(key)) {
return true
} else if (__DEV__) {
warn(`Invalid prop name: "${key}" is a reserved property.`)
}
return false
}
// dev only
2019-05-28 18:06:00 +08:00
// use function string name to check type constructors
// so that it works across vms / iframes.
function getType(ctor: Prop<any> | null): string {
// Early return for null to avoid unnecessary computations
if (ctor === null) {
return 'null'
}
// Avoid using regex for common cases by checking the type directly
if (typeof ctor === 'function') {
// Using name property to avoid converting function to string
return ctor.name || ''
} else if (typeof ctor === 'object') {
// Attempting to directly access constructor name if possible
const name = ctor.constructor && ctor.constructor.name
return name || ''
}
// Fallback for other types (though they're less likely to have meaningful names here)
return ''
2019-05-28 18:06:00 +08:00
}
/**
* dev only
2024-12-05 16:14:24 +08:00
* @internal
*/
2024-12-05 16:14:24 +08:00
export function validateProps(
rawProps: Data,
2024-12-05 16:14:24 +08:00
resolvedProps: Data,
options: NormalizedProps,
): void {
resolvedProps = toRaw(resolvedProps)
const camelizePropsKey = Object.keys(rawProps).map(key => camelize(key))
for (const key in options) {
2024-12-05 17:50:09 +08:00
const opt = options[key]
if (opt != null) {
validateProp(
key,
resolvedProps[key],
opt,
resolvedProps,
!camelizePropsKey.includes(key),
)
}
}
}
/**
* dev only
*/
2019-05-28 18:06:00 +08:00
function validateProp(
2024-12-05 16:14:24 +08:00
key: string,
2019-10-22 23:26:48 +08:00
value: unknown,
2024-12-05 16:14:24 +08:00
propOptions: PropOptions,
resolvedProps: Data,
2019-05-28 18:06:00 +08:00
isAbsent: boolean,
) {
2024-12-05 16:14:24 +08:00
const { type, required, validator, skipCheck } = propOptions
2019-05-28 18:06:00 +08:00
// required!
if (required && isAbsent) {
2024-12-05 16:14:24 +08:00
warn('Missing required prop: "' + key + '"')
2019-05-28 18:06:00 +08:00
return
}
// missing but optional
if (value == null && !required) {
2019-05-28 18:06:00 +08:00
return
}
// type check
if (type != null && type !== true && !skipCheck) {
2019-05-28 18:06:00 +08:00
let isValid = false
const types = 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) {
2024-12-05 16:14:24 +08:00
warn(getInvalidTypeMessage(key, value, expectedTypes))
2019-05-28 18:06:00 +08:00
return
}
}
// custom validator
2024-12-05 17:50:09 +08:00
if (
validator &&
!validator(value, __DEV__ ? shallowReadonly(resolvedProps) : resolvedProps)
) {
2024-12-05 16:14:24 +08:00
warn('Invalid prop: custom validator check failed for prop "' + key + '".')
2019-05-28 18:06:00 +08:00
}
}
const isSimpleType = /*@__PURE__*/ makeMap(
'String,Number,Boolean,Function,Symbol,BigInt',
)
2019-05-28 18:06:00 +08:00
type AssertionResult = {
valid: boolean
expectedType: string
}
/**
* dev only
*/
function assertType(
value: unknown,
type: PropConstructor | null,
): AssertionResult {
2019-05-28 18:06:00 +08:00
let valid
const expectedType = getType(type)
if (expectedType === 'null') {
valid = value === null
} else if (isSimpleType(expectedType)) {
2019-05-28 18:06:00 +08:00
const t = typeof value
valid = t === expectedType.toLowerCase()
// for primitive wrapper objects
if (!valid && t === 'object') {
valid = value instanceof (type as PropConstructor)
2019-05-28 18:06:00 +08:00
}
} else if (expectedType === 'Object') {
valid = isObject(value)
2019-05-28 18:06:00 +08:00
} else if (expectedType === 'Array') {
valid = isArray(value)
} else {
valid = value instanceof (type as PropConstructor)
2019-05-28 18:06:00 +08:00
}
return {
valid,
expectedType,
}
}
/**
* dev only
*/
2019-05-28 18:06:00 +08:00
function getInvalidTypeMessage(
name: string,
2019-10-22 23:26:48 +08:00
value: unknown,
2019-05-28 18:06:00 +08:00
expectedTypes: string[],
): string {
if (expectedTypes.length === 0) {
return (
`Prop type [] for prop "${name}" won't match anything.` +
` Did you mean to use type Array instead?`
)
}
2019-05-28 18:06:00 +08:00
let message =
`Invalid prop: type check failed for prop "${name}".` +
` Expected ${expectedTypes.map(capitalize).join(' | ')}`
2019-05-28 18:06:00 +08:00
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
}
/**
* dev only
*/
2019-10-22 23:26:48 +08:00
function styleValue(value: unknown, type: string): string {
2019-05-28 18:06:00 +08:00
if (type === 'String') {
return `"${value}"`
} else if (type === 'Number') {
return `${Number(value)}`
} else {
return `${value}`
}
}
/**
* dev only
*/
2019-05-28 18:06:00 +08:00
function isExplicable(type: string): boolean {
const explicitTypes = ['string', 'number', 'boolean']
return explicitTypes.some(elem => type.toLowerCase() === elem)
}
/**
* dev only
*/
2019-05-28 18:06:00 +08:00
function isBoolean(...args: string[]): boolean {
return args.some(elem => elem.toLowerCase() === 'boolean')
}