diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index b6f56f162cb..2b7b29b03a8 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -32,17 +32,17 @@ const readonlyGet = /*#__PURE__*/ createGetter(true) const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true) const arrayInstrumentations: Record = {} -;['includes', 'indexOf', 'lastIndexOf'].forEach(key => { - arrayInstrumentations[key] = function(...args: any[]): any { - const arr = toRaw(this) as any - for (let i = 0, l = (this as any).length; i < l; i++) { +;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => { + arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) { + const arr = toRaw(this) + for (let i = 0, l = this.length; i < l; i++) { track(arr, TrackOpTypes.GET, i + '') } // we run the method using the original args first (which may be reactive) - const res = arr[key](...args) + const res = (arr[key] as any)(...args) if (res === -1 || res === false) { // if that didn't work, run it again using raw values. - return arr[key](...args.map(toRaw)) + return (arr[key] as any)(...args.map(toRaw)) } else { return res } diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index c054e65a8eb..6fd77bc6e44 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -6,22 +6,65 @@ import { ComponentOptionsWithObjectProps, ComponentOptionsMixin, RenderFunction, - UnwrapAsyncBindings + ComponentOptionsBase } from './componentOptions' import { SetupContext, - FunctionalComponent, AllowedComponentProps, ComponentCustomProps } from './component' -import { - CreateComponentPublicInstance, - ComponentPublicInstanceConstructor -} from './componentPublicInstance' import { ExtractPropTypes, ComponentPropsOptions } from './componentProps' import { EmitsOptions } from './componentEmits' import { isFunction } from '@vue/shared' import { VNodeProps } from './vnode' +import { + CreateComponentPublicInstance, + ComponentPublicInstanceConstructor +} from './componentPublicInstance' + +export type PublicProps = VNodeProps & + AllowedComponentProps & + ComponentCustomProps + +export type DefineComponent< + PropsOrPropOptions = any, + RawBindings = any, + D = any, + C extends ComputedOptions = ComputedOptions, + M extends MethodOptions = MethodOptions, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, + E extends EmitsOptions = Record, + EE extends string = string, + PP = PublicProps, + RequiredProps = Readonly>, + OptionalProps = Readonly> +> = ComponentPublicInstanceConstructor< + CreateComponentPublicInstance< + OptionalProps, + RawBindings, + D, + C, + M, + Mixin, + Extends, + E, + PP & OptionalProps + > & + RequiredProps +> & + ComponentOptionsBase< + RequiredProps, + RawBindings, + D, + C, + M, + Mixin, + Extends, + E, + EE + > & + PP // defineComponent is a utility that is primarily used for type inference // when declaring components. Type inference is provided in the component @@ -35,21 +78,7 @@ export function defineComponent( props: Readonly, ctx: SetupContext ) => RawBindings | RenderFunction -): ComponentPublicInstanceConstructor< - CreateComponentPublicInstance< - Props, - UnwrapAsyncBindings, - {}, - {}, - {}, - {}, - {}, - {}, - // public props - VNodeProps & Props & AllowedComponentProps & ComponentCustomProps - > -> & - FunctionalComponent +): DefineComponent // overload 2: object format with no props // (uses user defined props interface) @@ -58,11 +87,11 @@ export function defineComponent< Props = {}, RawBindings = {}, D = {}, - C extends ComputedOptions = {}, - M extends MethodOptions = {}, + C extends ComputedOptions = ComputedOptions, + M extends MethodOptions = MethodOptions, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, - E extends EmitsOptions = Record, + E extends EmitsOptions = EmitsOptions, EE extends string = string >( options: ComponentOptionsWithoutProps< @@ -76,30 +105,7 @@ export function defineComponent< E, EE > -): ComponentPublicInstanceConstructor< - CreateComponentPublicInstance< - Props, - UnwrapAsyncBindings, - D, - C, - M, - Mixin, - Extends, - E, - VNodeProps & Props & AllowedComponentProps & ComponentCustomProps - > -> & - ComponentOptionsWithoutProps< - Props, - RawBindings, - D, - C, - M, - Mixin, - Extends, - E, - EE - > +): DefineComponent // overload 3: object format with array props declaration // props inferred as { [key in PropNames]?: any } @@ -126,32 +132,17 @@ export function defineComponent< E, EE > -): ComponentPublicInstanceConstructor< - // array props technically doesn't place any constraints on props in TSX before, - // but now we can export array props in TSX - CreateComponentPublicInstance< - Readonly<{ [key in PropNames]?: any }>, - UnwrapAsyncBindings, - D, - C, - M, - Mixin, - Extends, - E, - AllowedComponentProps & ComponentCustomProps - > -> & - ComponentOptionsWithArrayProps< - PropNames, - RawBindings, - D, - C, - M, - Mixin, - Extends, - E, - EE - > +): DefineComponent< + Readonly<{ [key in PropNames]?: any }>, + RawBindings, + D, + C, + M, + Mixin, + Extends, + E, + EE +> // overload 4: object format with object props declaration // see `ExtractPropTypes` in ./componentProps.ts @@ -179,33 +170,20 @@ export function defineComponent< E, EE > -): ComponentPublicInstanceConstructor< - CreateComponentPublicInstance< - ExtractPropTypes, - UnwrapAsyncBindings, - D, - C, - M, - Mixin, - Extends, - E, - VNodeProps & AllowedComponentProps & ComponentCustomProps - > & - Readonly> -> & - ComponentOptionsWithObjectProps< - PropsOptions, - RawBindings, - D, - C, - M, - Mixin, - Extends, - E, - EE - > +): DefineComponent // implementation, close to no-op export function defineComponent(options: unknown) { return isFunction(options) ? { setup: options, name: options.name } : options } + +defineComponent({ + async setup() { + return { + a: 123 + } + }, + render() { + this.a + } +}) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 6a768e26a09..5a74541eb74 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -26,7 +26,12 @@ import { warn } from './warning' import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { AppContext, createAppContext, AppConfig } from './apiCreateApp' import { Directive, validateDirectiveName } from './directives' -import { applyOptions, ComponentOptions } from './componentOptions' +import { + applyOptions, + ComponentOptions, + ComputedOptions, + MethodOptions +} from './componentOptions' import { EmitsOptions, ObjectEmitsOptions, @@ -118,13 +123,29 @@ export interface ClassComponent { * values, e.g. checking if its a function or not. This is mostly for internal * implementation code. */ -export type ConcreteComponent = ComponentOptions | FunctionalComponent +export type ConcreteComponent< + Props = {}, + RawBindings = any, + D = any, + C extends ComputedOptions = ComputedOptions, + M extends MethodOptions = MethodOptions +> = + | ComponentOptions + | FunctionalComponent /** * 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 type Component< + Props = any, + RawBindings = any, + D = any, + C extends ComputedOptions = ComputedOptions, + M extends MethodOptions = MethodOptions +> = + | ConcreteComponent + | ComponentPublicInstanceConstructor export { ComponentOptions } diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index dec584ae8b4..4d6291c9527 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -72,8 +72,6 @@ export interface ComponentCustomOptions {} export type RenderFunction = () => VNodeChild -export type UnwrapAsyncBindings = T extends Promise ? S : T - export interface ComponentOptionsBase< Props, RawBindings, @@ -92,7 +90,7 @@ export interface ComponentOptionsBase< this: void, props: Props, ctx: SetupContext - ) => RawBindings | RenderFunction | void + ) => Promise | RawBindings | RenderFunction | void name?: string template?: string | object // can be a direct DOM node // Note: we are intentionally using the signature-less `Function` type here @@ -230,10 +228,29 @@ export type ComponentOptionsWithObjectProps< > > -export type ComponentOptions = - | ComponentOptionsWithoutProps - | ComponentOptionsWithObjectProps - | ComponentOptionsWithArrayProps +export type ComponentOptions< + Props = {}, + RawBindings = any, + D = any, + C extends ComputedOptions = any, + M extends MethodOptions = any, + Mixin extends ComponentOptionsMixin = any, + Extends extends ComponentOptionsMixin = any, + E extends EmitsOptions = any +> = ComponentOptionsBase & + ThisType< + CreateComponentPublicInstance< + {}, + RawBindings, + D, + C, + M, + Mixin, + Extends, + E, + Readonly + > + > export type ComponentOptionsMixin = ComponentOptionsBase< any, @@ -638,17 +655,13 @@ export function applyOptions( onRenderTriggered(renderTriggered.bind(publicThis)) } if (__DEV__ && beforeDestroy) { - warn( - `\`beforeDestroy\` has been renamed to \`beforeUnmount\`.` - ) + warn(`\`beforeDestroy\` has been renamed to \`beforeUnmount\`.`) } if (beforeUnmount) { onBeforeUnmount(beforeUnmount.bind(publicThis)) } if (__DEV__ && destroyed) { - warn( - `\`destroyed\` has been renamed to \`unmounted\`.` - ) + warn(`\`destroyed\` has been renamed to \`unmounted\`.`) } if (unmounted) { onUnmounted(unmounted.bind(publicThis)) diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 77fcbeeae26..2a9fa747339 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -27,8 +27,7 @@ import { OptionTypesType, OptionTypesKeys, resolveMergedOptions, - isInBeforeCreate, - UnwrapAsyncBindings + isInBeforeCreate } from './componentOptions' import { EmitsOptions, EmitFn } from './componentEmits' import { Slots } from './componentSlots' @@ -102,7 +101,18 @@ type UnwrapMixinsType< type EnsureNonVoid = T extends void ? {} : T export type ComponentPublicInstanceConstructor< - T extends ComponentPublicInstance = ComponentPublicInstance + T extends ComponentPublicInstance< + Props, + RawBindings, + D, + C, + M + > = ComponentPublicInstance, + Props = any, + RawBindings = any, + D = any, + C extends ComputedOptions = ComputedOptions, + M extends MethodOptions = MethodOptions > = { __isFragment?: never __isTeleport?: never @@ -138,6 +148,7 @@ export type CreateComponentPublicInstance< PublicProps, ComponentOptionsBase > + // public properties exposed on the proxy, which is used as the render context // in templates (as `this` in the render option) export type ComponentPublicInstance< @@ -169,7 +180,7 @@ export type ComponentPublicInstance< options?: WatchOptions ): WatchStopHandle } & P & - ShallowUnwrapRef> & + ShallowUnwrapRef & D & ExtractComputedReturns & M & diff --git a/packages/runtime-core/src/h.ts b/packages/runtime-core/src/h.ts index 1939475b4ef..bf03247f2cb 100644 --- a/packages/runtime-core/src/h.ts +++ b/packages/runtime-core/src/h.ts @@ -10,9 +10,9 @@ import { Teleport, TeleportProps } from './components/Teleport' import { Suspense, SuspenseProps } from './components/Suspense' import { isObject, isArray } from '@vue/shared' import { RawSlots } from './componentSlots' -import { FunctionalComponent, Component } from './component' -import { ComponentOptions } from './componentOptions' +import { FunctionalComponent, Component, ComponentOptions } from './component' import { EmitsOptions } from './componentEmits' +import { DefineComponent } from './apiDefineComponent' // `h` is a more user-friendly version of `createVNode` that allows omitting the // props when possible. It is intended for manually written render functions. @@ -50,7 +50,7 @@ type RawProps = VNodeProps & { __v_isVNode?: never // used to differ from Array children [Symbol.iterator]?: never -} & { [key: string]: any } +} & Record type RawChildren = | string @@ -112,10 +112,17 @@ export function h( // catch-all for generic component types export function h(type: Component, children?: RawChildren): VNode +// component without props +export function h( + type: Component, + props: null, + children?: RawChildren | RawSlots +): VNode + // exclude `defineComponent` constructors -export function h>( - type: T, - props?: RawProps | null, +export function h

( + type: ComponentOptions

, + props?: (RawProps & P) | ({} extends P ? null : never), children?: RawChildren | RawSlots ): VNode @@ -127,6 +134,14 @@ export function h

( children?: RawChildren | RawSlots ): VNode +// fake constructor type returned by `defineComponent` +export function h(type: DefineComponent, children?: RawChildren): VNode +export function h

( + type: DefineComponent

, + props?: (RawProps & P) | ({} extends P ? null : never), + children?: RawChildren | RawSlots +): VNode + // Actual implementation export function h(type: any, propsOrChildren?: any, children?: any): VNode { const l = arguments.length diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index e60c0a99397..94492661113 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -41,7 +41,7 @@ export { } from './apiLifecycle' export { provide, inject } from './apiInject' export { nextTick } from './scheduler' -export { defineComponent } from './apiDefineComponent' +export { defineComponent, DefineComponent } from './apiDefineComponent' export { defineAsyncComponent } from './apiAsyncComponent' // Advanced API ---------------------------------------------------------------- diff --git a/test-dts/component.test-d.ts b/test-dts/component.test-d.ts new file mode 100644 index 00000000000..990e370518b --- /dev/null +++ b/test-dts/component.test-d.ts @@ -0,0 +1,413 @@ +import { + describe, + Component, + defineComponent, + PropType, + ref, + Ref, + expectError, + expectType, + ShallowUnwrapRef, + FunctionalComponent, + ComponentPublicInstance +} from './index' + +declare function extractComponentOptions( + obj: Component +): { + props: Props + rawBindings: RawBindings + setup: ShallowUnwrapRef +} + +describe('object props', () => { + interface ExpectedProps { + a?: number | undefined + b: string + e?: Function + bb: string + bbb: string + cc?: string[] | undefined + dd: { n: 1 } + ee?: () => string + ff?: (a: number, b: string) => { a: boolean } + ccc?: string[] | undefined + ddd: string[] + eee: () => { a: string } + fff: (a: number, b: string) => { a: boolean } + hhh: boolean + ggg: 'foo' | 'bar' + ffff: (a: number, b: string) => { a: boolean } + validated?: string + } + + describe('defineComponent', () => { + const MyComponent = defineComponent({ + props: { + a: Number, + // required should make property non-void + b: { + type: String, + required: true + }, + e: Function, + // default value should infer type and make it non-void + bb: { + default: 'hello' + }, + bbb: { + // Note: default function value requires arrow syntax + explicit + // annotation + default: (props: any) => (props.bb as string) || 'foo' + }, + // explicit type casting + cc: Array as PropType, + // required + type casting + dd: { + type: Object as PropType<{ n: 1 }>, + required: true + }, + // return type + ee: Function as PropType<() => string>, + // arguments + object return + ff: Function as PropType<(a: number, b: string) => { a: boolean }>, + // explicit type casting with constructor + ccc: Array as () => string[], + // required + contructor type casting + ddd: { + type: Array as () => string[], + required: true + }, + // required + object return + eee: { + type: Function as PropType<() => { a: string }>, + required: true + }, + // required + arguments + object return + fff: { + type: Function as PropType<(a: number, b: string) => { a: boolean }>, + required: true + }, + hhh: { + type: Boolean, + required: true + }, + // default + type casting + ggg: { + type: String as PropType<'foo' | 'bar'>, + default: 'foo' + }, + // default + function + ffff: { + type: Function as PropType<(a: number, b: string) => { a: boolean }>, + default: (_a: number, _b: string) => ({ a: true }) + }, + validated: { + type: String, + // validator requires explicit annotation + validator: (val: unknown) => val !== '' + } + }, + setup(props) { + return { + setupA: 1, + setupB: ref(1), + setupC: { + a: ref(2) + }, + setupProps: props + } + } + }) + + const { props, rawBindings, setup } = extractComponentOptions(MyComponent) + + // props + expectType(props.a) + expectType(props.b) + expectType(props.e) + expectType(props.bb) + expectType(props.bbb) + expectType(props.cc) + expectType(props.dd) + expectType(props.ee) + expectType(props.ff) + expectType(props.ccc) + expectType(props.ddd) + expectType(props.eee) + expectType(props.fff) + expectType(props.hhh) + expectType(props.ggg) + expectType(props.ffff) + expectType(props.validated) + + // raw bindings + expectType(rawBindings.setupA) + expectType>(rawBindings.setupB) + expectType>(rawBindings.setupC.a) + expectType(rawBindings.setupA) + + // raw bindings props + expectType(rawBindings.setupProps.a) + expectType(rawBindings.setupProps.b) + expectType(rawBindings.setupProps.e) + expectType(rawBindings.setupProps.bb) + expectType(rawBindings.setupProps.bbb) + expectType(rawBindings.setupProps.cc) + expectType(rawBindings.setupProps.dd) + expectType(rawBindings.setupProps.ee) + expectType(rawBindings.setupProps.ff) + expectType(rawBindings.setupProps.ccc) + expectType(rawBindings.setupProps.ddd) + expectType(rawBindings.setupProps.eee) + expectType(rawBindings.setupProps.fff) + expectType(rawBindings.setupProps.hhh) + expectType(rawBindings.setupProps.ggg) + expectType(rawBindings.setupProps.ffff) + expectType(rawBindings.setupProps.validated) + + // setup + expectType(setup.setupA) + expectType(setup.setupB) + expectType>(setup.setupC.a) + expectType(setup.setupA) + + // raw bindings props + expectType(setup.setupProps.a) + expectType(setup.setupProps.b) + expectType(setup.setupProps.e) + expectType(setup.setupProps.bb) + expectType(setup.setupProps.bbb) + expectType(setup.setupProps.cc) + expectType(setup.setupProps.dd) + expectType(setup.setupProps.ee) + expectType(setup.setupProps.ff) + expectType(setup.setupProps.ccc) + expectType(setup.setupProps.ddd) + expectType(setup.setupProps.eee) + expectType(setup.setupProps.fff) + expectType(setup.setupProps.hhh) + expectType(setup.setupProps.ggg) + expectType(setup.setupProps.ffff) + expectType(setup.setupProps.validated) + }) + + describe('options', () => { + const MyComponent = { + props: { + a: Number, + // required should make property non-void + b: { + type: String, + required: true + }, + e: Function, + // default value should infer type and make it non-void + bb: { + default: 'hello' + }, + bbb: { + // Note: default function value requires arrow syntax + explicit + // annotation + default: (props: any) => (props.bb as string) || 'foo' + }, + // explicit type casting + cc: Array as PropType, + // required + type casting + dd: { + type: Object as PropType<{ n: 1 }>, + required: true + }, + // return type + ee: Function as PropType<() => string>, + // arguments + object return + ff: Function as PropType<(a: number, b: string) => { a: boolean }>, + // explicit type casting with constructor + ccc: Array as () => string[], + // required + contructor type casting + ddd: { + type: Array as () => string[], + required: true + }, + // required + object return + eee: { + type: Function as PropType<() => { a: string }>, + required: true + }, + // required + arguments + object return + fff: { + type: Function as PropType<(a: number, b: string) => { a: boolean }>, + required: true + }, + hhh: { + type: Boolean, + required: true + }, + // default + type casting + ggg: { + type: String as PropType<'foo' | 'bar'>, + default: 'foo' + }, + // default + function + ffff: { + type: Function as PropType<(a: number, b: string) => { a: boolean }>, + default: (_a: number, _b: string) => ({ a: true }) + }, + validated: { + type: String, + // validator requires explicit annotation + validator: (val: unknown) => val !== '' + } + }, + + setup() { + return { + setupA: 1 + } + } + } as const + + const { props, rawBindings, setup } = extractComponentOptions(MyComponent) + + // props + expectType(props.a) + expectType(props.b) + expectType(props.e) + expectType(props.bb) + expectType(props.bbb) + expectType(props.cc) + expectType(props.dd) + expectType(props.ee) + expectType(props.ff) + expectType(props.ccc) + expectType(props.ddd) + expectType(props.eee) + expectType(props.fff) + expectType(props.hhh) + expectType(props.ggg) + // expectType(props.ffff) // todo fix + expectType(props.validated) + + // rawBindings + expectType(rawBindings.setupA) + + //setup + expectType(setup.setupA) + }) +}) + +describe('array props', () => { + describe('defineComponent', () => { + const MyComponent = defineComponent({ + props: ['a', 'b'], + setup() { + return { + c: 1 + } + } + }) + + const { props, rawBindings, setup } = extractComponentOptions(MyComponent) + + // @ts-expect-error props should be readonly + expectError((props.a = 1)) + expectType(props.a) + expectType(props.b) + + expectType(rawBindings.c) + expectType(setup.c) + }) + + describe('options', () => { + const MyComponent = { + props: ['a', 'b'] as const, + setup() { + return { + c: 1 + } + } + } + + const { props, rawBindings, setup } = extractComponentOptions(MyComponent) + + // @ts-expect-error props should be readonly + expectError((props.a = 1)) + + // TODO infer the correct keys + // expectType(props.a) + // expectType(props.b) + + expectType(rawBindings.c) + expectType(setup.c) + }) +}) + +describe('no props', () => { + describe('defineComponent', () => { + const MyComponent = defineComponent({ + setup() { + return { + setupA: 1 + } + } + }) + + const { rawBindings, setup } = extractComponentOptions(MyComponent) + + expectType(rawBindings.setupA) + expectType(setup.setupA) + }) + + describe('options', () => { + const MyComponent = { + setup() { + return { + setupA: 1 + } + } + } + + const { rawBindings, setup } = extractComponentOptions(MyComponent) + + expectType(rawBindings.setupA) + expectType(setup.setupA) + }) +}) + +describe('functional', () => { + // TODO `props.foo` is `number|undefined` + // describe('defineComponent', () => { + // const MyComponent = defineComponent((props: { foo: number }) => {}) + + // const { props } = extractComponentOptions(MyComponent) + + // expectType(props.foo) + // }) + + describe('function', () => { + const MyComponent = (props: { foo: number }) => props.foo + const { props } = extractComponentOptions(MyComponent) + + expectType(props.foo) + }) + + describe('typed', () => { + const MyComponent: FunctionalComponent<{ foo: number }> = (_, _2) => {} + + const { props } = extractComponentOptions(MyComponent) + + expectType(props.foo) + }) +}) + +declare type VueClass = { + new (): ComponentPublicInstance +} + +describe('class', () => { + const MyComponent: VueClass<{ foo: number }> = {} as any + + const { props } = extractComponentOptions(MyComponent) + + expectType(props.foo) +})