refactor(types): widen Component type to include consutructor types

returned from `defineComponent`

ref: https://github.com/vuejs/vue-router-next/pull/421
also close #1880

Previous `Component` type is now exported as `ConcreteComponent`.

This introduces a minor breaking change when calling `h(comp, { ... })`
will now fail if `comp` is a of generic `Component` type, since it does
not specify what props it expects.
This commit is contained in:
Evan You 2020-08-19 16:11:29 -04:00
parent 4baf852a34
commit eb2ae44d94
21 changed files with 117 additions and 102 deletions

View File

@ -11,8 +11,7 @@ import {
watch,
watchEffect,
onUnmounted,
onErrorCaptured,
Component
onErrorCaptured
} from '@vue/runtime-test'
describe('Suspense', () => {
@ -31,7 +30,7 @@ describe('Suspense', () => {
setup(props: any, { slots }: any) {
const p = new Promise(resolve => {
setTimeout(() => {
resolve(() => h<Component>(comp, props, slots))
resolve(() => h(comp, props, slots))
}, delay)
})
// in Node 12, due to timer/nextTick mechanism change, we have to wait

View File

@ -1,21 +1,19 @@
import {
PublicAPIComponent,
Component,
ConcreteComponent,
currentInstance,
ComponentInternalInstance,
isInSSRComponentSetup
} from './component'
import { isFunction, isObject } from '@vue/shared'
import { ComponentPublicInstance } from './componentProxy'
import { ComponentPublicInstance } from './componentPublicInstance'
import { createVNode } from './vnode'
import { defineComponent } from './apiDefineComponent'
import { warn } from './warning'
import { ref } from '@vue/reactivity'
import { handleError, ErrorCodes } from './errorHandling'
export type AsyncComponentResolveResult<T = PublicAPIComponent> =
| T
| { default: T } // es modules
export type AsyncComponentResolveResult<T = Component> = T | { default: T } // es modules
export type AsyncComponentLoader<T = any> = () => Promise<
AsyncComponentResolveResult<T>
@ -23,8 +21,8 @@ export type AsyncComponentLoader<T = any> = () => Promise<
export interface AsyncComponentOptions<T = any> {
loader: AsyncComponentLoader<T>
loadingComponent?: PublicAPIComponent
errorComponent?: PublicAPIComponent
loadingComponent?: Component
errorComponent?: Component
delay?: number
timeout?: number
suspensible?: boolean
@ -37,7 +35,7 @@ export interface AsyncComponentOptions<T = any> {
}
export function defineAsyncComponent<
T extends PublicAPIComponent = { new (): ComponentPublicInstance }
T extends Component = { new (): ComponentPublicInstance }
>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
if (isFunction(source)) {
source = { loader: source }
@ -53,8 +51,8 @@ export function defineAsyncComponent<
onError: userOnError
} = source
let pendingRequest: Promise<Component> | null = null
let resolvedComp: Component | undefined
let pendingRequest: Promise<ConcreteComponent> | null = null
let resolvedComp: ConcreteComponent | undefined
let retries = 0
const retry = () => {
@ -63,8 +61,8 @@ export function defineAsyncComponent<
return load()
}
const load = (): Promise<Component> => {
let thisRequest: Promise<Component>
const load = (): Promise<ConcreteComponent> => {
let thisRequest: Promise<ConcreteComponent>
return (
pendingRequest ||
(thisRequest = pendingRequest = loader()
@ -135,7 +133,9 @@ export function defineAsyncComponent<
onError(err)
return () =>
errorComponent
? createVNode(errorComponent as Component, { error: err })
? createVNode(errorComponent as ConcreteComponent, {
error: err
})
: null
})
}
@ -175,11 +175,11 @@ export function defineAsyncComponent<
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance)
} else if (error.value && errorComponent) {
return createVNode(errorComponent as Component, {
return createVNode(errorComponent as ConcreteComponent, {
error: error.value
})
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent as Component)
return createVNode(loadingComponent as ConcreteComponent)
}
}
}
@ -187,7 +187,7 @@ export function defineAsyncComponent<
}
function createInnerComp(
comp: Component,
comp: ConcreteComponent,
{ vnode: { props, children } }: ComponentInternalInstance
) {
return createVNode(comp, props, children)

View File

@ -1,11 +1,11 @@
import {
Component,
ConcreteComponent,
Data,
validateComponentName,
PublicAPIComponent
Component
} from './component'
import { ComponentOptions } from './componentOptions'
import { ComponentPublicInstance } from './componentProxy'
import { ComponentPublicInstance } from './componentPublicInstance'
import { Directive, validateDirectiveName } from './directives'
import { RootRenderFunction } from './renderer'
import { InjectionKey } from './apiInject'
@ -21,8 +21,8 @@ export interface App<HostElement = any> {
config: AppConfig
use(plugin: Plugin, ...options: any[]): this
mixin(mixin: ComponentOptions): this
component(name: string): PublicAPIComponent | undefined
component(name: string, component: PublicAPIComponent): this
component(name: string): Component | undefined
component(name: string, component: Component): this
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): this
mount(
@ -33,7 +33,7 @@ export interface App<HostElement = any> {
provide<T>(key: InjectionKey<T> | string, value: T): this
// internal, but we need to expose these for the server-renderer and devtools
_component: Component
_component: ConcreteComponent
_props: Data | null
_container: HostElement | null
_context: AppContext
@ -70,7 +70,7 @@ export interface AppContext {
app: App // for devtools
config: AppConfig
mixins: ComponentOptions[]
components: Record<string, PublicAPIComponent>
components: Record<string, Component>
directives: Record<string, Directive>
provides: Record<string | symbol, any>
reload?: () => void // HMR only
@ -104,7 +104,7 @@ export function createAppContext(): AppContext {
}
export type CreateAppFunction<HostElement> = (
rootComponent: PublicAPIComponent,
rootComponent: Component,
rootProps?: Data | null
) => App<HostElement>
@ -124,7 +124,7 @@ export function createAppAPI<HostElement>(
let isMounted = false
const app: App = (context.app = {
_component: rootComponent as Component,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
@ -177,7 +177,7 @@ export function createAppAPI<HostElement>(
return app
},
component(name: string, component?: PublicAPIComponent): any {
component(name: string, component?: Component): any {
if (__DEV__) {
validateComponentName(name, context.config)
}
@ -208,7 +208,10 @@ export function createAppAPI<HostElement>(
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
const vnode = createVNode(rootComponent as Component, rootProps)
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context

View File

@ -16,7 +16,7 @@ import {
import {
CreateComponentPublicInstance,
ComponentPublicInstanceConstructor
} from './componentProxy'
} from './componentPublicInstance'
import { ExtractPropTypes, ComponentPropsOptions } from './componentProps'
import { EmitsOptions } from './componentEmits'
import { isFunction } from '@vue/shared'
@ -205,7 +205,5 @@ export function defineComponent<
// implementation, close to no-op
export function defineComponent(options: unknown) {
return isFunction(options)
? { setup: options, name: options.name }
: options
return isFunction(options) ? { setup: options, name: options.name } : options
}

View File

@ -5,7 +5,7 @@ import {
setCurrentInstance,
isInSSRComponentSetup
} from './component'
import { ComponentPublicInstance } from './componentProxy'
import { ComponentPublicInstance } from './componentPublicInstance'
import { callWithAsyncErrorHandling, ErrorTypeStrings } from './errorHandling'
import { warn } from './warning'
import { capitalize } from '@vue/shared'

View File

@ -7,14 +7,14 @@ import {
proxyRefs
} from '@vue/reactivity'
import {
CreateComponentPublicInstance,
ComponentPublicInstance,
PublicInstanceProxyHandlers,
RuntimeCompiledPublicInstanceProxyHandlers,
createRenderContext,
exposePropsOnRenderContext,
exposeSetupStateOnRenderContext
} from './componentProxy'
exposeSetupStateOnRenderContext,
ComponentPublicInstanceConstructor
} from './componentPublicInstance'
import {
ComponentPropsOptions,
NormalizedPropsOptions,
@ -110,21 +110,19 @@ export interface ClassComponent {
__vccOpts: ComponentOptions
}
export type Component = ComponentOptions | FunctionalComponent<any, any>
/**
* Concrete component type matches its actual value: it's either an options
* object, or a function. Use this where the code expects to work with actual
* values, e.g. checking if its a function or not. This is mostly for internal
* implementation code.
*/
export type ConcreteComponent = ComponentOptions | FunctionalComponent<any, any>
// A type used in public APIs where a component type is expected.
// The constructor type is an artificial type returned by defineComponent().
export type PublicAPIComponent =
| Component
| {
new (...args: any[]): CreateComponentPublicInstance<
any,
any,
any,
any,
any
>
}
/**
* A type used in public APIs where a component type is expected.
* The constructor type is an artificial type returned by defineComponent().
*/
export type Component = ConcreteComponent | ComponentPublicInstanceConstructor
export { ComponentOptions }
@ -174,7 +172,7 @@ export type InternalRenderFunction = {
*/
export interface ComponentInternalInstance {
uid: number
type: Component
type: ConcreteComponent
parent: ComponentInternalInstance | null
root: ComponentInternalInstance
appContext: AppContext
@ -346,7 +344,7 @@ export function createComponentInstance(
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null
) {
const type = vnode.type as Component
const type = vnode.type as ConcreteComponent
// inherit parent app context - or - if root, adopt from root vnode
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
@ -703,7 +701,7 @@ const classify = (str: string): string =>
/* istanbul ignore next */
export function formatComponentName(
instance: ComponentInternalInstance | null,
Component: Component,
Component: ConcreteComponent,
isRoot = false
): string {
let name = isFunction(Component)

View File

@ -8,7 +8,7 @@ import {
isFunction,
extend
} from '@vue/shared'
import { ComponentInternalInstance, Component } from './component'
import { ComponentInternalInstance, ConcreteComponent } from './component'
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
import { warn } from './warning'
import { normalizePropsOptions } from './componentProps'
@ -94,7 +94,7 @@ export function emit(
}
function normalizeEmitsOptions(
comp: Component
comp: ConcreteComponent
): ObjectEmitsOptions | undefined {
if (hasOwn(comp, '__emits')) {
return comp.__emits
@ -131,7 +131,7 @@ function normalizeEmitsOptions(
// Check if an incoming prop key is a declared emit event listener.
// e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
// both considered matched listeners.
export function isEmitListener(comp: Component, key: string): boolean {
export function isEmitListener(comp: ConcreteComponent, key: string): boolean {
let emits: ObjectEmitsOptions | undefined
if (!isOn(key) || !(emits = normalizeEmitsOptions(comp))) {
return false

View File

@ -3,8 +3,8 @@ import {
Data,
SetupContext,
ComponentInternalOptions,
PublicAPIComponent,
Component,
ConcreteComponent,
InternalRenderFunction
} from './component'
import {
@ -52,7 +52,7 @@ import { Directive } from './directives'
import {
CreateComponentPublicInstance,
ComponentPublicInstance
} from './componentProxy'
} from './componentPublicInstance'
import { warn } from './warning'
import { VNodeChild } from './vnode'
@ -103,7 +103,7 @@ export interface ComponentOptionsBase<
// Luckily `render()` doesn't need any arguments nor does it care about return
// type.
render?: Function
components?: Record<string, PublicAPIComponent>
components?: Record<string, Component>
directives?: Record<string, Directive>
inheritAttrs?: boolean
emits?: (E | EE[]) & ThisType<void>
@ -132,7 +132,7 @@ export interface ComponentOptionsBase<
* marker for AsyncComponentWrapper
* @internal
*/
__asyncLoader?: () => Promise<Component>
__asyncLoader?: () => Promise<ConcreteComponent>
/**
* cache for merged $options
* @internal

View File

@ -27,7 +27,7 @@ import {
Data,
ComponentInternalInstance,
ComponentOptions,
Component
ConcreteComponent
} from './component'
import { isEmitListener } from './componentEmits'
import { InternalObjectKey } from './vnode'
@ -310,7 +310,7 @@ function resolvePropValue(
}
export function normalizePropsOptions(
comp: Component
comp: ConcreteComponent
): NormalizedPropsOptions | [] {
if (comp.__props) {
return comp.__props
@ -411,7 +411,7 @@ function getTypeIndex(
/**
* dev only
*/
function validateProps(props: Data, comp: Component) {
function validateProps(props: Data, comp: ConcreteComponent) {
const rawValues = toRaw(props)
const options = normalizePropsOptions(comp)[0]
for (const key in options) {

View File

@ -101,6 +101,15 @@ type UnwrapMixinsType<
type EnsureNonVoid<T> = T extends void ? {} : T
export type ComponentPublicInstanceConstructor<
T extends ComponentPublicInstance = ComponentPublicInstance<any>
> = {
__isFragment?: never
__isTeleport?: never
__isSuspense?: never
new (): T
}
export type CreateComponentPublicInstance<
P = {},
B = {},
@ -162,12 +171,6 @@ export type ComponentPublicInstance<
M &
ComponentCustomProperties
export type ComponentPublicInstanceConstructor<
T extends ComponentPublicInstance
> = {
new (): T
}
type PublicPropertiesMap = Record<string, (i: ComponentInternalInstance) => any>
const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), {

View File

@ -1,5 +1,5 @@
import {
Component,
ConcreteComponent,
getCurrentInstance,
FunctionalComponent,
SetupContext,
@ -33,7 +33,7 @@ import {
invokeVNodeHook
} from '../renderer'
import { setTransitionHooks } from './BaseTransition'
import { ComponentRenderContext } from '../componentProxy'
import { ComponentRenderContext } from '../componentPublicInstance'
type MatchPattern = string | RegExp | string[] | RegExp[]
@ -43,7 +43,7 @@ export interface KeepAliveProps {
max?: number | string
}
type CacheKey = string | number | Component
type CacheKey = string | number | ConcreteComponent
type Cache = Map<CacheKey, VNode>
type Keys = Set<CacheKey>
@ -151,7 +151,7 @@ const KeepAliveImpl = {
function pruneCache(filter?: (name: string) => boolean) {
cache.forEach((vnode, key) => {
const name = getName(vnode.type as Component)
const name = getName(vnode.type as ConcreteComponent)
if (name && (!filter || !filter(name))) {
pruneCacheEntry(key)
}
@ -228,7 +228,7 @@ const KeepAliveImpl = {
return vnode
}
const comp = vnode.type as Component
const comp = vnode.type as ConcreteComponent
const name = getName(comp)
const { include, exclude, max } = props
@ -291,7 +291,7 @@ export const KeepAlive = (KeepAliveImpl as any) as {
}
}
function getName(comp: Component): string | void {
function getName(comp: ConcreteComponent): string | void {
return (comp as FunctionalComponent).displayName || comp.name
}

View File

@ -17,7 +17,7 @@ import { warn } from './warning'
import { ComponentInternalInstance, Data } from './component'
import { currentRenderingInstance } from './componentRenderUtils'
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
import { ComponentPublicInstance } from './componentProxy'
import { ComponentPublicInstance } from './componentPublicInstance'
export interface DirectiveBinding<V = any> {
instance: ComponentPublicInstance | null

View File

@ -68,11 +68,6 @@ interface Constructor<P = any> {
new (): { $props: P }
}
// Excludes Component type from returned `defineComponent`
type NotDefinedComponent<T extends Component> = T extends Constructor
? never
: T
// The following is a series of overloads for providing props validation of
// manually written render functions.
@ -117,9 +112,9 @@ export function h<P, E extends EmitsOptions = {}>(
// catch-all for generic component types
export function h(type: Component, children?: RawChildren): VNode
// exclude `defineComponent`
export function h<Options extends ComponentOptions | FunctionalComponent<{}>>(
type: NotDefinedComponent<Options>,
// exclude `defineComponent` constructors
export function h<T extends ComponentOptions | FunctionalComponent<{}>>(
type: T,
props?: RawProps | null,
children?: RawChildren | RawSlots
): VNode

View File

@ -1,7 +1,7 @@
import { currentRenderingInstance } from '../componentRenderUtils'
import {
currentInstance,
Component,
ConcreteComponent,
FunctionalComponent,
ComponentOptions
} from '../component'
@ -16,7 +16,9 @@ const DIRECTIVES = 'directives'
/**
* @private
*/
export function resolveComponent(name: string): Component | string | undefined {
export function resolveComponent(
name: string
): ConcreteComponent | string | undefined {
return resolveAsset(COMPONENTS, name) || name
}
@ -49,7 +51,7 @@ function resolveAsset(
type: typeof COMPONENTS,
name: string,
warnMissing?: boolean
): Component | undefined
): ConcreteComponent | undefined
// overload 2: directives
function resolveAsset(
type: typeof DIRECTIVES,

View File

@ -1,6 +1,6 @@
/* eslint-disable no-restricted-globals */
import {
Component,
ConcreteComponent,
ComponentInternalInstance,
ComponentOptions,
InternalRenderFunction
@ -10,7 +10,7 @@ import { extend } from '@vue/shared'
export let isHmrUpdating = false
export const hmrDirtyComponents = new Set<Component>()
export const hmrDirtyComponents = new Set<ConcreteComponent>()
export interface HMRRuntime {
createRecord: typeof createRecord

View File

@ -159,6 +159,7 @@ export {
} from './vnode'
export {
Component,
ConcreteComponent,
FunctionalComponent,
ComponentInternalInstance,
SetupContext,
@ -178,7 +179,7 @@ export {
export {
ComponentPublicInstance,
ComponentCustomProperties
} from './componentProxy'
} from './componentPublicInstance'
export {
Renderer,
RendererNode,

View File

@ -66,7 +66,7 @@ import {
import { createHydrationFunctions, RootHydrateFunction } from './hydration'
import { invokeDirectiveHook } from './directives'
import { startMeasure, endMeasure } from './profiling'
import { ComponentPublicInstance } from './componentProxy'
import { ComponentPublicInstance } from './componentPublicInstance'
import { devtoolsComponentRemoved, devtoolsComponentUpdated } from './devtools'
import { initFeatureFlags } from './featureFlags'

View File

@ -15,8 +15,9 @@ import {
import {
ComponentInternalInstance,
Data,
Component,
ClassComponent
ConcreteComponent,
ClassComponent,
Component
} from './component'
import { RawSlots } from './componentSlots'
import { isProxy, Ref, toRaw } from '@vue/reactivity'
@ -244,7 +245,7 @@ export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
if (
__DEV__ &&
n2.shapeFlag & ShapeFlags.COMPONENT &&
hmrDirtyComponents.has(n2.type as Component)
hmrDirtyComponents.has(n2.type as ConcreteComponent)
) {
// HMR only: if the component has been hot-updated, force a reload.
return false

View File

@ -2,7 +2,7 @@ import { VNode } from './vnode'
import {
Data,
ComponentInternalInstance,
Component,
ConcreteComponent,
formatComponentName
} from './component'
import { isString, isFunction } from '@vue/shared'
@ -10,7 +10,7 @@ import { toRaw, isRef, pauseTracking, resetTracking } from '@vue/reactivity'
import { callWithErrorHandling, ErrorCodes } from './errorHandling'
type ComponentVNode = VNode & {
type: Component
type: ConcreteComponent
}
const stack: VNode[] = []

View File

@ -1,5 +1,6 @@
import {
describe,
Component,
defineComponent,
PropType,
ref,
@ -179,6 +180,8 @@ describe('with object props', () => {
}
})
expectType<Component>(MyComponent)
// Test TSX
expectType<JSX.Element>(
<MyComponent
@ -205,6 +208,17 @@ describe('with object props', () => {
/>
)
expectType<Component>(
<MyComponent
b="b"
dd={{ n: 1 }}
ddd={['ddd']}
eee={() => ({ a: 'eee' })}
fff={(a, b) => ({ a: a > +b })}
hhh={false}
/>
)
// @ts-expect-error missing required props
expectError(<MyComponent />)

View File

@ -148,6 +148,7 @@ describe('h support for generic component type', () => {
function foo(bar: Component) {
h(bar)
h(bar, 'hello')
// @ts-expect-error
h(bar, { id: 'ok' }, 'hello')
}
foo({})