componentProps.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. import {
  2. TriggerOpTypes,
  3. shallowReactive,
  4. shallowReadonly,
  5. toRaw,
  6. trigger,
  7. } from '@vue/reactivity'
  8. import {
  9. EMPTY_ARR,
  10. EMPTY_OBJ,
  11. type IfAny,
  12. PatchFlags,
  13. camelize,
  14. capitalize,
  15. extend,
  16. hasOwn,
  17. hyphenate,
  18. isArray,
  19. isFunction,
  20. isObject,
  21. isOn,
  22. isReservedProp,
  23. isString,
  24. makeMap,
  25. toRawType,
  26. } from '@vue/shared'
  27. import { warn } from './warning'
  28. import {
  29. type ComponentInternalInstance,
  30. type ComponentOptions,
  31. type ConcreteComponent,
  32. type Data,
  33. type GenericComponentInstance,
  34. setCurrentInstance,
  35. } from './component'
  36. import { isEmitListener } from './componentEmits'
  37. import type { AppContext } from './apiCreateApp'
  38. import { createPropsDefaultThis } from './compat/props'
  39. import { isCompatEnabled, softAssertCompatEnabled } from './compat/compatConfig'
  40. import { DeprecationTypes } from './compat/compatConfig'
  41. import { shouldSkipAttr } from './compat/attrsFallthrough'
  42. import { createInternalObject } from './internalObject'
  43. export type ComponentPropsOptions<P = Data> =
  44. | ComponentObjectPropsOptions<P>
  45. | string[]
  46. export type ComponentObjectPropsOptions<P = Data> = {
  47. [K in keyof P]: Prop<P[K]> | null
  48. }
  49. export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
  50. type DefaultFactory<T> = (props: Data) => T | null | undefined
  51. export interface PropOptions<T = any, D = T> {
  52. type?: PropType<T> | true | null
  53. required?: boolean
  54. default?: D | DefaultFactory<D> | null | undefined | object
  55. validator?(value: unknown, props: Data): boolean
  56. /**
  57. * @internal
  58. */
  59. skipCheck?: boolean
  60. /**
  61. * @internal
  62. */
  63. skipFactory?: boolean
  64. }
  65. export type PropType<T> = PropConstructor<T> | (PropConstructor<T> | null)[]
  66. type PropConstructor<T = any> =
  67. | { new (...args: any[]): T & {} }
  68. | { (): T }
  69. | PropMethod<T>
  70. type PropMethod<T, TConstructor = any> = [T] extends [
  71. ((...args: any) => any) | undefined,
  72. ] // if is function with args, allowing non-required functions
  73. ? { new (): TConstructor; (): T; readonly prototype: TConstructor } // Create Function like constructor
  74. : never
  75. type RequiredKeys<T> = {
  76. [K in keyof T]: T[K] extends
  77. | { required: true }
  78. | { default: any }
  79. // don't mark Boolean props as undefined
  80. | BooleanConstructor
  81. | { type: BooleanConstructor }
  82. ? T[K] extends { default: undefined | (() => undefined) }
  83. ? never
  84. : K
  85. : never
  86. }[keyof T]
  87. type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>
  88. type DefaultKeys<T> = {
  89. [K in keyof T]: T[K] extends
  90. | { default: any }
  91. // Boolean implicitly defaults to false
  92. | BooleanConstructor
  93. | { type: BooleanConstructor }
  94. ? T[K] extends { type: BooleanConstructor; required: true } // not default if Boolean is marked as required
  95. ? never
  96. : K
  97. : never
  98. }[keyof T]
  99. type InferPropType<T, NullAsAny = true> = [T] extends [null]
  100. ? NullAsAny extends true
  101. ? any
  102. : null
  103. : [T] extends [{ type: null | true }]
  104. ? 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`
  105. : [T] extends [ObjectConstructor | { type: ObjectConstructor }]
  106. ? Record<string, any>
  107. : [T] extends [BooleanConstructor | { type: BooleanConstructor }]
  108. ? boolean
  109. : [T] extends [DateConstructor | { type: DateConstructor }]
  110. ? Date
  111. : [T] extends [(infer U)[] | { type: (infer U)[] }]
  112. ? U extends DateConstructor
  113. ? Date | InferPropType<U, false>
  114. : InferPropType<U, false>
  115. : [T] extends [Prop<infer V, infer D>]
  116. ? unknown extends V
  117. ? keyof V extends never
  118. ? IfAny<V, V, D>
  119. : V
  120. : V
  121. : T
  122. /**
  123. * Extract prop types from a runtime props options object.
  124. * The extracted types are **internal** - i.e. the resolved props received by
  125. * the component.
  126. * - Boolean props are always present
  127. * - Props with default values are always present
  128. *
  129. * To extract accepted props from the parent, use {@link ExtractPublicPropTypes}.
  130. */
  131. export type ExtractPropTypes<O> = {
  132. // use `keyof Pick<O, RequiredKeys<O>>` instead of `RequiredKeys<O>` to
  133. // support IDE features
  134. [K in keyof Pick<O, RequiredKeys<O>>]: InferPropType<O[K]>
  135. } & {
  136. // use `keyof Pick<O, OptionalKeys<O>>` instead of `OptionalKeys<O>` to
  137. // support IDE features
  138. [K in keyof Pick<O, OptionalKeys<O>>]?: InferPropType<O[K]>
  139. }
  140. type PublicRequiredKeys<T> = {
  141. [K in keyof T]: T[K] extends { required: true } ? K : never
  142. }[keyof T]
  143. type PublicOptionalKeys<T> = Exclude<keyof T, PublicRequiredKeys<T>>
  144. /**
  145. * Extract prop types from a runtime props options object.
  146. * The extracted types are **public** - i.e. the expected props that can be
  147. * passed to component.
  148. */
  149. export type ExtractPublicPropTypes<O> = {
  150. [K in keyof Pick<O, PublicRequiredKeys<O>>]: InferPropType<O[K]>
  151. } & {
  152. [K in keyof Pick<O, PublicOptionalKeys<O>>]?: InferPropType<O[K]>
  153. }
  154. enum BooleanFlags {
  155. shouldCast,
  156. shouldCastTrue,
  157. }
  158. // extract props which defined with default from prop options
  159. export type ExtractDefaultPropTypes<O> = O extends object
  160. ? // use `keyof Pick<O, DefaultKeys<O>>` instead of `DefaultKeys<O>` to support IDE features
  161. { [K in keyof Pick<O, DefaultKeys<O>>]: InferPropType<O[K]> }
  162. : {}
  163. type NormalizedProp = PropOptions & {
  164. [BooleanFlags.shouldCast]?: boolean
  165. [BooleanFlags.shouldCastTrue]?: boolean
  166. }
  167. /**
  168. * normalized value is a tuple of the actual normalized options
  169. * and an array of prop keys that need value casting (booleans and defaults)
  170. */
  171. export type NormalizedProps = Record<string, NormalizedProp>
  172. export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
  173. export function initProps(
  174. instance: ComponentInternalInstance,
  175. rawProps: Data | null,
  176. isStateful: number, // result of bitwise flag comparison
  177. isSSR = false,
  178. ): void {
  179. const props: Data = (instance.props = {})
  180. const attrs: Data = createInternalObject()
  181. instance.propsDefaults = Object.create(null)
  182. setFullProps(instance, rawProps, props, attrs)
  183. // ensure all declared prop keys are present
  184. for (const key in instance.propsOptions[0]) {
  185. if (!(key in props)) {
  186. props[key] = undefined
  187. }
  188. }
  189. // validation
  190. if (__DEV__) {
  191. validateProps(rawProps || {}, props, instance.propsOptions[0]!)
  192. }
  193. if (isStateful) {
  194. // stateful
  195. instance.props = isSSR ? props : shallowReactive(props)
  196. } else {
  197. if (!instance.type.props) {
  198. // functional w/ optional props, props === attrs
  199. instance.props = attrs
  200. } else {
  201. // functional w/ declared props
  202. instance.props = props
  203. }
  204. }
  205. instance.attrs = attrs
  206. }
  207. function isInHmrContext(instance: GenericComponentInstance | null) {
  208. while (instance) {
  209. if (instance.type.__hmrId) return true
  210. instance = instance.parent
  211. }
  212. }
  213. export function updateProps(
  214. instance: ComponentInternalInstance,
  215. rawProps: Data | null,
  216. rawPrevProps: Data | null,
  217. optimized: boolean,
  218. ): void {
  219. const {
  220. props,
  221. attrs,
  222. vnode: { patchFlag },
  223. } = instance
  224. const rawCurrentProps = toRaw(props)
  225. const [options] = instance.propsOptions
  226. let hasAttrsChanged = false
  227. if (
  228. // always force full diff in dev
  229. // - #1942 if hmr is enabled with sfc component
  230. // - vite#872 non-sfc component used by sfc component
  231. !(__DEV__ && isInHmrContext(instance)) &&
  232. (optimized || patchFlag > 0) &&
  233. !(patchFlag & PatchFlags.FULL_PROPS)
  234. ) {
  235. if (patchFlag & PatchFlags.PROPS) {
  236. // Compiler-generated props & no keys change, just set the updated
  237. // the props.
  238. const propsToUpdate = instance.vnode.dynamicProps!
  239. for (let i = 0; i < propsToUpdate.length; i++) {
  240. let key = propsToUpdate[i]
  241. // skip if the prop key is a declared emit event listener
  242. if (isEmitListener(instance.emitsOptions, key)) {
  243. continue
  244. }
  245. // PROPS flag guarantees rawProps to be non-null
  246. const value = rawProps![key]
  247. if (options) {
  248. // attr / props separation was done on init and will be consistent
  249. // in this code path, so just check if attrs have it.
  250. if (hasOwn(attrs, key)) {
  251. if (value !== attrs[key]) {
  252. attrs[key] = value
  253. hasAttrsChanged = true
  254. }
  255. } else {
  256. const camelizedKey = camelize(key)
  257. props[camelizedKey] = resolvePropValue(
  258. options,
  259. camelizedKey,
  260. value,
  261. instance,
  262. baseResolveDefault,
  263. )
  264. }
  265. } else {
  266. if (__COMPAT__) {
  267. if (isOn(key) && key.endsWith('Native')) {
  268. key = key.slice(0, -6) // remove Native postfix
  269. } else if (shouldSkipAttr(key, instance)) {
  270. continue
  271. }
  272. }
  273. if (value !== attrs[key]) {
  274. attrs[key] = value
  275. hasAttrsChanged = true
  276. }
  277. }
  278. }
  279. }
  280. } else {
  281. // full props update.
  282. if (setFullProps(instance, rawProps, props, attrs)) {
  283. hasAttrsChanged = true
  284. }
  285. // in case of dynamic props, check if we need to delete keys from
  286. // the props object
  287. let kebabKey: string
  288. for (const key in rawCurrentProps) {
  289. if (
  290. !rawProps ||
  291. // for camelCase
  292. (!hasOwn(rawProps, key) &&
  293. // it's possible the original props was passed in as kebab-case
  294. // and converted to camelCase (#955)
  295. ((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))
  296. ) {
  297. if (options) {
  298. if (
  299. rawPrevProps &&
  300. // for camelCase
  301. (rawPrevProps[key] !== undefined ||
  302. // for kebab-case
  303. rawPrevProps[kebabKey!] !== undefined)
  304. ) {
  305. props[key] = resolvePropValue(
  306. options,
  307. key,
  308. undefined,
  309. instance,
  310. baseResolveDefault,
  311. true /* isAbsent */,
  312. )
  313. }
  314. } else {
  315. delete props[key]
  316. }
  317. }
  318. }
  319. // in the case of functional component w/o props declaration, props and
  320. // attrs point to the same object so it should already have been updated.
  321. if (attrs !== rawCurrentProps) {
  322. for (const key in attrs) {
  323. if (
  324. !rawProps ||
  325. (!hasOwn(rawProps, key) &&
  326. (!__COMPAT__ || !hasOwn(rawProps, key + 'Native')))
  327. ) {
  328. delete attrs[key]
  329. hasAttrsChanged = true
  330. }
  331. }
  332. }
  333. }
  334. // trigger updates for $attrs in case it's used in component slots
  335. if (hasAttrsChanged) {
  336. trigger(instance.attrs, TriggerOpTypes.SET, '')
  337. }
  338. if (__DEV__) {
  339. validateProps(rawProps || {}, props, instance.propsOptions[0]!)
  340. }
  341. }
  342. function setFullProps(
  343. instance: ComponentInternalInstance,
  344. rawProps: Data | null,
  345. props: Data,
  346. attrs: Data,
  347. ) {
  348. const [options, needCastKeys] = instance.propsOptions
  349. let hasAttrsChanged = false
  350. let rawCastValues: Data | undefined
  351. if (rawProps) {
  352. for (let key in rawProps) {
  353. // key, ref are reserved and never passed down
  354. if (isReservedProp(key)) {
  355. continue
  356. }
  357. if (__COMPAT__) {
  358. if (key.startsWith('onHook:')) {
  359. softAssertCompatEnabled(
  360. DeprecationTypes.INSTANCE_EVENT_HOOKS,
  361. instance,
  362. key.slice(2).toLowerCase(),
  363. )
  364. }
  365. if (key === 'inline-template') {
  366. continue
  367. }
  368. }
  369. const value = rawProps[key]
  370. // prop option names are camelized during normalization, so to support
  371. // kebab -> camel conversion here we need to camelize the key.
  372. let camelKey
  373. if (options && hasOwn(options, (camelKey = camelize(key)))) {
  374. if (!needCastKeys || !needCastKeys.includes(camelKey)) {
  375. props[camelKey] = value
  376. } else {
  377. ;(rawCastValues || (rawCastValues = {}))[camelKey] = value
  378. }
  379. } else if (!isEmitListener(instance.emitsOptions, key)) {
  380. // Any non-declared (either as a prop or an emitted event) props are put
  381. // into a separate `attrs` object for spreading. Make sure to preserve
  382. // original key casing
  383. if (__COMPAT__) {
  384. if (isOn(key) && key.endsWith('Native')) {
  385. key = key.slice(0, -6) // remove Native postfix
  386. } else if (shouldSkipAttr(key, instance)) {
  387. continue
  388. }
  389. }
  390. if (!(key in attrs) || value !== attrs[key]) {
  391. attrs[key] = value
  392. hasAttrsChanged = true
  393. }
  394. }
  395. }
  396. }
  397. if (needCastKeys) {
  398. const castValues = rawCastValues || EMPTY_OBJ
  399. for (let i = 0; i < needCastKeys.length; i++) {
  400. const key = needCastKeys[i]
  401. props[key] = resolvePropValue(
  402. options!,
  403. key,
  404. castValues[key],
  405. instance,
  406. baseResolveDefault,
  407. !hasOwn(castValues, key),
  408. )
  409. }
  410. }
  411. return hasAttrsChanged
  412. }
  413. /**
  414. * @internal for runtime-vapor
  415. */
  416. export function resolvePropValue<
  417. T extends GenericComponentInstance & Pick<ComponentInternalInstance, 'ce'>,
  418. >(
  419. options: NormalizedProps,
  420. key: string,
  421. value: unknown,
  422. instance: T,
  423. /**
  424. * Allow runtime-specific default resolution logic
  425. */
  426. resolveDefault: (
  427. factory: (props: Data) => unknown,
  428. instance: T,
  429. key: string,
  430. ) => unknown,
  431. isAbsent = false,
  432. ): unknown {
  433. const opt = options[key]
  434. if (opt != null) {
  435. const hasDefault = hasOwn(opt, 'default')
  436. // default values
  437. if (hasDefault && value === undefined) {
  438. const defaultValue = opt.default
  439. if (
  440. opt.type !== Function &&
  441. !opt.skipFactory &&
  442. isFunction(defaultValue)
  443. ) {
  444. const cachedDefaults =
  445. instance.propsDefaults || (instance.propsDefaults = {})
  446. if (hasOwn(cachedDefaults, key)) {
  447. value = cachedDefaults[key]
  448. } else {
  449. value = cachedDefaults[key] = resolveDefault(
  450. defaultValue,
  451. instance,
  452. key,
  453. )
  454. }
  455. } else {
  456. value = defaultValue
  457. }
  458. // #9006 reflect default value on custom element
  459. if (instance.ce) {
  460. instance.ce._setProp(key, value)
  461. }
  462. }
  463. // boolean casting
  464. if (opt[BooleanFlags.shouldCast]) {
  465. if (isAbsent && !hasDefault) {
  466. value = false
  467. } else if (
  468. opt[BooleanFlags.shouldCastTrue] &&
  469. (value === '' || value === hyphenate(key))
  470. ) {
  471. value = true
  472. }
  473. }
  474. }
  475. return value
  476. }
  477. /**
  478. * runtime-dom-specific default resolving logic
  479. */
  480. function baseResolveDefault(
  481. factory: (props: Data) => unknown,
  482. instance: ComponentInternalInstance,
  483. key: string,
  484. ) {
  485. let value
  486. const reset = setCurrentInstance(instance)
  487. const props = toRaw(instance.props)
  488. value = factory.call(
  489. __COMPAT__ && isCompatEnabled(DeprecationTypes.PROPS_DEFAULT_THIS, instance)
  490. ? createPropsDefaultThis(instance, props, key)
  491. : null,
  492. props,
  493. )
  494. reset()
  495. return value
  496. }
  497. const mixinPropsCache = new WeakMap<ConcreteComponent, NormalizedPropsOptions>()
  498. export function normalizePropsOptions(
  499. comp: ConcreteComponent,
  500. appContext: AppContext,
  501. asMixin = false,
  502. ): NormalizedPropsOptions {
  503. const cache =
  504. __FEATURE_OPTIONS_API__ && asMixin ? mixinPropsCache : appContext.propsCache
  505. const cached = cache.get(comp)
  506. if (cached) {
  507. return cached
  508. }
  509. const raw = comp.props
  510. const normalized: NormalizedPropsOptions[0] = {}
  511. const needCastKeys: NormalizedPropsOptions[1] = []
  512. // apply mixin/extends props
  513. let hasExtends = false
  514. if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
  515. const extendProps = (raw: ComponentOptions) => {
  516. if (__COMPAT__ && isFunction(raw)) {
  517. raw = raw.options
  518. }
  519. hasExtends = true
  520. const [props, keys] = normalizePropsOptions(raw, appContext, true)
  521. extend(normalized, props)
  522. if (keys) needCastKeys.push(...keys)
  523. }
  524. if (!asMixin && appContext.mixins.length) {
  525. appContext.mixins.forEach(extendProps)
  526. }
  527. if (comp.extends) {
  528. extendProps(comp.extends)
  529. }
  530. if (comp.mixins) {
  531. comp.mixins.forEach(extendProps)
  532. }
  533. }
  534. if (!raw && !hasExtends) {
  535. if (isObject(comp)) {
  536. cache.set(comp, EMPTY_ARR as any)
  537. }
  538. return EMPTY_ARR as any
  539. }
  540. baseNormalizePropsOptions(raw, normalized, needCastKeys)
  541. const res: NormalizedPropsOptions = [normalized, needCastKeys]
  542. if (isObject(comp)) {
  543. cache.set(comp, res)
  544. }
  545. return res
  546. }
  547. /**
  548. * @internal for runtime-vapor only
  549. */
  550. export function baseNormalizePropsOptions(
  551. raw: ComponentPropsOptions | undefined,
  552. normalized: NonNullable<NormalizedPropsOptions[0]>,
  553. needCastKeys: NonNullable<NormalizedPropsOptions[1]>,
  554. ): void {
  555. if (isArray(raw)) {
  556. for (let i = 0; i < raw.length; i++) {
  557. if (__DEV__ && !isString(raw[i])) {
  558. warn(`props must be strings when using array syntax.`, raw[i])
  559. }
  560. const normalizedKey = camelize(raw[i])
  561. if (validatePropName(normalizedKey)) {
  562. normalized[normalizedKey] = EMPTY_OBJ
  563. }
  564. }
  565. } else if (raw) {
  566. if (__DEV__ && !isObject(raw)) {
  567. warn(`invalid props options`, raw)
  568. }
  569. for (const key in raw) {
  570. const normalizedKey = camelize(key)
  571. if (validatePropName(normalizedKey)) {
  572. const opt = raw[key]
  573. const prop: NormalizedProp = (normalized[normalizedKey] =
  574. isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt))
  575. const propType = prop.type
  576. let shouldCast = false
  577. let shouldCastTrue = true
  578. if (isArray(propType)) {
  579. for (let index = 0; index < propType.length; ++index) {
  580. const type = propType[index]
  581. const typeName = isFunction(type) && type.name
  582. if (typeName === 'Boolean') {
  583. shouldCast = true
  584. break
  585. } else if (typeName === 'String') {
  586. // If we find `String` before `Boolean`, e.g. `[String, Boolean]`,
  587. // we need to handle the casting slightly differently. Props
  588. // passed as `<Comp checked="">` or `<Comp checked="checked">`
  589. // will either be treated as strings or converted to a boolean
  590. // `true`, depending on the order of the types.
  591. shouldCastTrue = false
  592. }
  593. }
  594. } else {
  595. shouldCast = isFunction(propType) && propType.name === 'Boolean'
  596. }
  597. prop[BooleanFlags.shouldCast] = shouldCast
  598. prop[BooleanFlags.shouldCastTrue] = shouldCastTrue
  599. // if the prop needs boolean casting or default value
  600. if (shouldCast || hasOwn(prop, 'default')) {
  601. needCastKeys.push(normalizedKey)
  602. }
  603. }
  604. }
  605. }
  606. }
  607. function validatePropName(key: string) {
  608. if (key[0] !== '$' && !isReservedProp(key)) {
  609. return true
  610. } else if (__DEV__) {
  611. warn(`Invalid prop name: "${key}" is a reserved property.`)
  612. }
  613. return false
  614. }
  615. // dev only
  616. // use function string name to check type constructors
  617. // so that it works across vms / iframes.
  618. function getType(ctor: Prop<any> | null): string {
  619. // Early return for null to avoid unnecessary computations
  620. if (ctor === null) {
  621. return 'null'
  622. }
  623. // Avoid using regex for common cases by checking the type directly
  624. if (typeof ctor === 'function') {
  625. // Using name property to avoid converting function to string
  626. return ctor.name || ''
  627. } else if (typeof ctor === 'object') {
  628. // Attempting to directly access constructor name if possible
  629. const name = ctor.constructor && ctor.constructor.name
  630. return name || ''
  631. }
  632. // Fallback for other types (though they're less likely to have meaningful names here)
  633. return ''
  634. }
  635. /**
  636. * dev only
  637. * @internal
  638. */
  639. export function validateProps(
  640. rawProps: Data,
  641. resolvedProps: Data,
  642. options: NormalizedProps,
  643. ): void {
  644. resolvedProps = toRaw(resolvedProps)
  645. const camelizePropsKey = Object.keys(rawProps).map(key => camelize(key))
  646. for (const key in options) {
  647. const opt = options[key]
  648. if (opt != null) {
  649. validateProp(
  650. key,
  651. resolvedProps[key],
  652. opt,
  653. resolvedProps,
  654. !camelizePropsKey.includes(key),
  655. )
  656. }
  657. }
  658. }
  659. /**
  660. * dev only
  661. */
  662. function validateProp(
  663. key: string,
  664. value: unknown,
  665. propOptions: PropOptions,
  666. resolvedProps: Data,
  667. isAbsent: boolean,
  668. ) {
  669. const { type, required, validator, skipCheck } = propOptions
  670. // required!
  671. if (required && isAbsent) {
  672. warn('Missing required prop: "' + key + '"')
  673. return
  674. }
  675. // missing but optional
  676. if (value == null && !required) {
  677. return
  678. }
  679. // type check
  680. if (type != null && type !== true && !skipCheck) {
  681. let isValid = false
  682. const types = isArray(type) ? type : [type]
  683. const expectedTypes = []
  684. // value is valid as long as one of the specified types match
  685. for (let i = 0; i < types.length && !isValid; i++) {
  686. const { valid, expectedType } = assertType(value, types[i])
  687. expectedTypes.push(expectedType || '')
  688. isValid = valid
  689. }
  690. if (!isValid) {
  691. warn(getInvalidTypeMessage(key, value, expectedTypes))
  692. return
  693. }
  694. }
  695. // custom validator
  696. if (
  697. validator &&
  698. !validator(value, __DEV__ ? shallowReadonly(resolvedProps) : resolvedProps)
  699. ) {
  700. warn('Invalid prop: custom validator check failed for prop "' + key + '".')
  701. }
  702. }
  703. const isSimpleType = /*@__PURE__*/ makeMap(
  704. 'String,Number,Boolean,Function,Symbol,BigInt',
  705. )
  706. type AssertionResult = {
  707. valid: boolean
  708. expectedType: string
  709. }
  710. /**
  711. * dev only
  712. */
  713. function assertType(
  714. value: unknown,
  715. type: PropConstructor | null,
  716. ): AssertionResult {
  717. let valid
  718. const expectedType = getType(type)
  719. if (expectedType === 'null') {
  720. valid = value === null
  721. } else if (isSimpleType(expectedType)) {
  722. const t = typeof value
  723. valid = t === expectedType.toLowerCase()
  724. // for primitive wrapper objects
  725. if (!valid && t === 'object') {
  726. valid = value instanceof (type as PropConstructor)
  727. }
  728. } else if (expectedType === 'Object') {
  729. valid = isObject(value)
  730. } else if (expectedType === 'Array') {
  731. valid = isArray(value)
  732. } else {
  733. valid = value instanceof (type as PropConstructor)
  734. }
  735. return {
  736. valid,
  737. expectedType,
  738. }
  739. }
  740. /**
  741. * dev only
  742. */
  743. function getInvalidTypeMessage(
  744. name: string,
  745. value: unknown,
  746. expectedTypes: string[],
  747. ): string {
  748. if (expectedTypes.length === 0) {
  749. return (
  750. `Prop type [] for prop "${name}" won't match anything.` +
  751. ` Did you mean to use type Array instead?`
  752. )
  753. }
  754. let message =
  755. `Invalid prop: type check failed for prop "${name}".` +
  756. ` Expected ${expectedTypes.map(capitalize).join(' | ')}`
  757. const expectedType = expectedTypes[0]
  758. const receivedType = toRawType(value)
  759. const expectedValue = styleValue(value, expectedType)
  760. const receivedValue = styleValue(value, receivedType)
  761. // check if we need to specify expected value
  762. if (
  763. expectedTypes.length === 1 &&
  764. isExplicable(expectedType) &&
  765. !isBoolean(expectedType, receivedType)
  766. ) {
  767. message += ` with value ${expectedValue}`
  768. }
  769. message += `, got ${receivedType} `
  770. // check if we need to specify received value
  771. if (isExplicable(receivedType)) {
  772. message += `with value ${receivedValue}.`
  773. }
  774. return message
  775. }
  776. /**
  777. * dev only
  778. */
  779. function styleValue(value: unknown, type: string): string {
  780. if (type === 'String') {
  781. return `"${value}"`
  782. } else if (type === 'Number') {
  783. return `${Number(value)}`
  784. } else {
  785. return `${value}`
  786. }
  787. }
  788. /**
  789. * dev only
  790. */
  791. function isExplicable(type: string): boolean {
  792. const explicitTypes = ['string', 'number', 'boolean']
  793. return explicitTypes.some(elem => type.toLowerCase() === elem)
  794. }
  795. /**
  796. * dev only
  797. */
  798. function isBoolean(...args: string[]): boolean {
  799. return args.some(elem => elem.toLowerCase() === 'boolean')
  800. }