diff --git a/src/types/component.ts b/src/types/component.ts index 538f7c3a1c..f28f446087 100644 --- a/src/types/component.ts +++ b/src/types/component.ts @@ -1,6 +1,7 @@ import type VNode from 'core/vdom/vnode' import type Watcher from 'core/observer/watcher' -import { ComponentOptions, SetupContext } from './options' +import { ComponentOptions } from './options' +import { SetupContext } from 'v3/apiSetup' import { ScopedSlotsData, VNodeChildren, VNodeData } from './vnode' import { GlobalAPI } from './global-api' import { EffectScope } from 'v3/reactivity/effectScope' diff --git a/src/types/options.ts b/src/types/options.ts index 76bebd1a7f..ee4440db96 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -1,5 +1,6 @@ import VNode from 'core/vdom/vnode' -import { DebuggerEvent } from 'v3' +import { DebuggerEvent } from 'v3/debug' +import { SetupContext } from 'v3/apiSetup' import { Component } from './component' export type InternalComponentOptions = { @@ -12,15 +13,6 @@ export type InternalComponentOptions = { type InjectKey = string | Symbol -/** - * @internal - */ -export interface SetupContext { - attrs: Record - slots: Record VNode[]> - emit: (event: string, ...args: any[]) => any -} - /** * @internal */ diff --git a/src/v3/apiSetup.ts b/src/v3/apiSetup.ts index c1abe64f4c..92c1425fd2 100644 --- a/src/v3/apiSetup.ts +++ b/src/v3/apiSetup.ts @@ -1,11 +1,19 @@ import { Component } from 'types/component' -import type { SetupContext } from 'types/options' import { def, invokeWithErrorHandling, isReserved, warn } from '../core/util' import VNode from '../core/vdom/vnode' import { bind, emptyObject, isFunction, isObject } from '../shared/util' import { currentInstance, setCurrentInstance } from './currentInstance' import { isRef } from './reactivity/ref' +/** + * @internal + */ +export interface SetupContext { + attrs: Record + slots: Record VNode[]> + emit: (event: string, ...args: any[]) => any +} + export function initSetup(vm: Component) { const options = vm.$options const setup = options.setup diff --git a/src/v3/index.ts b/src/v3/index.ts index 97fdc709b2..2e6a71c5bf 100644 --- a/src/v3/index.ts +++ b/src/v3/index.ts @@ -74,4 +74,11 @@ export { useSlots, useAttrs } from './apiSetup' export { nextTick } from 'core/util/next-tick' export { set, del } from 'core/observer' +/** + * @internal type is manually declared in /types/v3-define-component.d.ts + */ +export function defineComponent(options: any) { + return options +} + export * from './apiLifecycle' diff --git a/types/common.d.ts b/types/common.d.ts new file mode 100644 index 0000000000..7b69ddd647 --- /dev/null +++ b/types/common.d.ts @@ -0,0 +1,15 @@ +export type Data = { [key: string]: unknown } + +export type UnionToIntersection = ( + U extends any ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never + +// Conditional returns can enforce identical types. +// See here: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650 +// prettier-ignore +type Equal = + (() => U extends Left ? 1 : 0) extends (() => U extends Right ? 1 : 0) ? true : false; + +export type HasDefined = Equal extends true ? false : true diff --git a/types/index.d.ts b/types/index.d.ts index eea3c51c57..b724d798e9 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -11,8 +11,8 @@ export { ComponentOptions, FunctionalComponentOptions, RenderContext, - PropType, - PropOptions, + // PropType, + // PropOptions, ComputedOptions, WatchHandler, WatchOptions, @@ -32,5 +32,36 @@ export { VNodeDirective } from './vnode' -export * from './v3' +export * from './v3-manual-apis' export * from './v3-generated' + +export { Data } from './common' +export { SetupContext } from './v3-setup-context' +export { defineComponent } from './v3-define-component' +// export { defineAsyncComponent } from './defineAsyncComponent' +export { + SetupFunction, + // v2 already has option with same name and it's for a single computed + ComputedOptions as ComponentComputedOptions, + MethodOptions as ComponentMethodOptions, + ComponentPropsOptions +} from './v3-component-options' +export { + ComponentInstance, + ComponentPublicInstance, + ComponentRenderProxy +} from './v3-component-proxy' +export { + PropType, + PropOptions, + ExtractPropTypes, + ExtractDefaultPropTypes +} from './v3-component-props' +export { + DirectiveModifiers, + DirectiveBinding, + DirectiveHook, + ObjectDirective, + FunctionDirective, + Directive +} from './v3-directive' diff --git a/types/options.d.ts b/types/options.d.ts index e01c1ba6d3..8868e7cf87 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -1,6 +1,7 @@ import { Vue, CreateElement, CombinedVueInstance } from './vue' import { VNode, VNodeData, VNodeDirective, NormalizedScopedSlot } from './vnode' -import { SetupContext } from './v3' +import { SetupContext } from './v3-setup-context' +import { DebuggerEvent } from './v3-generated' type Constructor = { new (...args: any[]): any @@ -249,22 +250,10 @@ export interface RenderContext { injections: any } -export type Prop = - | { (): T } - | { new (...args: never[]): T & object } - | { new (...args: string[]): Function } - -export type PropType = Prop | Prop[] +import { PropOptions, PropType } from './v3-component-props' export type PropValidator = PropOptions | PropType -export interface PropOptions { - type?: PropType - required?: boolean - default?: T | null | undefined | (() => T | null | undefined) - validator?(value: T): boolean -} - export type RecordPropsDefinition = { [K in keyof T]: PropValidator } @@ -316,24 +305,3 @@ export type InjectOptions = [key: string]: InjectKey | { from?: InjectKey; default?: any } } | string[] - -export type DebuggerEvent = { - target: object - type: TrackOpTypes | TriggerOpTypes - key?: any - newValue?: any - oldValue?: any - oldTarget?: Map | Set -} - -export const enum TrackOpTypes { - GET = 'get', - TOUCH = 'touch' -} - -export const enum TriggerOpTypes { - SET = 'set', - ADD = 'add', - DELETE = 'delete', - ARRAY_MUTATION = 'array mutation' -} diff --git a/types/test/v3/setup-test.ts b/types/test/v3/setup-test.ts index d09fdf278c..8ad8eefe40 100644 --- a/types/test/v3/setup-test.ts +++ b/types/test/v3/setup-test.ts @@ -1,4 +1,4 @@ -import Vue from '../../index' +import Vue, { defineComponent } from '../../index' // object props Vue.extend({ @@ -30,3 +30,51 @@ Vue.extend({ ctx.slots.default && ctx.slots.default() } }) + +// object props +defineComponent({ + props: { + foo: String, + bar: Number + }, + setup(props) { + // @ts-expect-error + props.foo.slice(1, 2) + + props.foo?.slice(1, 2) + + // @ts-expect-error + props.bar + 123 + + props.bar?.toFixed(2) + } +}) + +// array props +defineComponent({ + props: ['foo', 'bar'], + setup(props) { + props.foo + props.bar + } +}) + +// context +defineComponent({ + emits: ['foo'], + setup(_props, ctx) { + if (ctx.attrs.id) { + } + ctx.emit('foo') + // @ts-expect-error + ctx.emit('ok') + ctx.slots.default && ctx.slots.default() + }, + methods: { + foo() { + this.$emit('foo') + // @ts-expect-error + this.$emit('bar') + } + } +}) diff --git a/types/tsconfig.json b/types/tsconfig.json index 55df289717..d8c4327e7f 100644 --- a/types/tsconfig.json +++ b/types/tsconfig.json @@ -12,6 +12,6 @@ "vue": ["../index.d.ts"] } }, - "include": ["./*.d.ts", "*.ts"], + "include": ["."], "compileOnSave": false } diff --git a/types/v3-component-options.d.ts b/types/v3-component-options.d.ts new file mode 100644 index 0000000000..d6e63ed87a --- /dev/null +++ b/types/v3-component-options.d.ts @@ -0,0 +1,120 @@ +import { Vue } from './vue' +import { VNode } from './vnode' +import { ComponentOptions as Vue2ComponentOptions } from './options' +import { EmitsOptions, SetupContext } from './v3-setup-context' +import { Data } from './common' +import { ComponentPropsOptions, ExtractPropTypes } from './v3-component-props' +import { ComponentRenderProxy } from './v3-component-proxy' +export { ComponentPropsOptions } from './v3-component-props' + +export type ComputedGetter = (ctx?: any) => T +export type ComputedSetter = (v: T) => void + +export interface WritableComputedOptions { + get: ComputedGetter + set: ComputedSetter +} + +export type ComputedOptions = Record< + string, + ComputedGetter | WritableComputedOptions +> + +export interface MethodOptions { + [key: string]: Function +} + +export type SetupFunction< + Props, + RawBindings = {}, + Emits extends EmitsOptions = {} +> = ( + this: void, + props: Readonly, + ctx: SetupContext +) => RawBindings | (() => VNode | null) | void + +interface ComponentOptionsBase< + Props, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {} +> extends Omit< + Vue2ComponentOptions, + 'data' | 'computed' | 'method' | 'setup' | 'props' + > { + // allow any custom options + [key: string]: any + + // rewrite options api types + data?: (this: Props & Vue, vm: Props) => D + computed?: C + methods?: M +} + +export type ExtractComputedReturns = { + [key in keyof T]: T[key] extends { get: (...args: any[]) => infer TReturn } + ? TReturn + : T[key] extends (...args: any[]) => infer TReturn + ? TReturn + : never +} + +export type ComponentOptionsWithProps< + PropsOptions = ComponentPropsOptions, + RawBindings = Data, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + EmitsNames extends string = string, + Props = ExtractPropTypes +> = ComponentOptionsBase & { + props?: PropsOptions + emits?: (Emits | EmitsNames[]) & ThisType + setup?: SetupFunction +} & ThisType< + ComponentRenderProxy + > + +export type ComponentOptionsWithArrayProps< + PropNames extends string = string, + RawBindings = Data, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + EmitsNames extends string = string, + Props = Readonly<{ [key in PropNames]?: any }> +> = ComponentOptionsBase & { + props?: PropNames[] + emits?: (Emits | EmitsNames[]) & ThisType + setup?: SetupFunction +} & ThisType< + ComponentRenderProxy + > + +export type ComponentOptionsWithoutProps< + Props = {}, + RawBindings = Data, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + EmitsNames extends string = string +> = ComponentOptionsBase & { + props?: undefined + emits?: (Emits | EmitsNames[]) & ThisType + setup?: SetupFunction +} & ThisType< + ComponentRenderProxy + > + +export type WithLegacyAPI = T & + Omit, keyof T> diff --git a/types/v3-component-props.d.ts b/types/v3-component-props.d.ts new file mode 100644 index 0000000000..7abf0d4b8d --- /dev/null +++ b/types/v3-component-props.d.ts @@ -0,0 +1,100 @@ +import { Data } from './common' + +export type ComponentPropsOptions

= + | ComponentObjectPropsOptions

+ | string[] + +export type ComponentObjectPropsOptions

= { + [K in keyof P]: Prop | null +} + +export type Prop = PropOptions | PropType + +type DefaultFactory = () => T | null | undefined + +export interface PropOptions { + type?: PropType | true | null + required?: boolean + default?: D | DefaultFactory | null | undefined | object + validator?(value: unknown): boolean +} + +export type PropType = PropConstructor | PropConstructor[] + +type PropConstructor = + | { new (...args: any[]): T & object } + | { (): T } + | { new (...args: string[]): Function } + +type RequiredKeys = { + [K in keyof T]: T[K] extends + | { required: true } + | { default: any } + | BooleanConstructor + | { type: BooleanConstructor } + ? K + : never +}[keyof T] + +type OptionalKeys = Exclude> + +type ExtractFunctionPropType< + T extends Function, + TArgs extends Array = any[], + TResult = any +> = T extends (...args: TArgs) => TResult ? T : never + +type ExtractCorrectPropType = T extends Function + ? ExtractFunctionPropType + : Exclude + +// prettier-ignore +type InferPropType = T extends null + ? any // null & true would fail to infer + : 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 + : T extends BooleanConstructor | { type: BooleanConstructor } + ? boolean + : T extends DateConstructor | { type: DateConstructor} + ? Date + : T extends FunctionConstructor + ? Function + : T extends Prop + ? unknown extends V + ? D extends null | undefined + ? V + : D + : ExtractCorrectPropType + : T + +export type ExtractPropTypes = { + // use `keyof Pick>` instead of `RequiredKeys` to support IDE features + [K in keyof Pick>]: InferPropType +} & { + // use `keyof Pick>` instead of `OptionalKeys` to support IDE features + [K in keyof Pick>]?: InferPropType +} + +type DefaultKeys = { + [K in keyof T]: T[K] extends + | { + default: any + } + | BooleanConstructor + | { type: BooleanConstructor } + ? T[K] extends { + type: BooleanConstructor + required: true + } + ? never + : K + : never +}[keyof T] + +// extract props which defined with default from prop options +export type ExtractDefaultPropTypes = O extends object + ? // use `keyof Pick>` instead of `DefaultKeys` to support IDE features + { [K in keyof Pick>]: InferPropType } + : {} diff --git a/types/v3-component-proxy.d.ts b/types/v3-component-proxy.d.ts new file mode 100644 index 0000000000..d1f73452ed --- /dev/null +++ b/types/v3-component-proxy.d.ts @@ -0,0 +1,189 @@ +import { ExtractDefaultPropTypes, ExtractPropTypes } from './v3-component-props' +import { + nextTick, + ShallowUnwrapRef, + UnwrapNestedRefs, + WatchOptions, + WatchStopHandle +} from './v3-generated' +import { Data } from './common' + +import { Vue, VueConstructor } from './vue' +import { ComponentOptions as Vue2ComponentOptions } from './options' +import { + ComputedOptions, + MethodOptions, + ExtractComputedReturns +} from './v3-component-options' +import { + ComponentRenderEmitFn, + EmitFn, + EmitsOptions, + ObjectEmitsOptions, + Slots +} from './v3-setup-context' + +type EmitsToProps = T extends string[] + ? { + [K in string & `on${Capitalize}`]?: (...args: any[]) => any + } + : T extends ObjectEmitsOptions + ? { + [K in string & + `on${Capitalize}`]?: K extends `on${infer C}` + ? T[Uncapitalize] extends null + ? (...args: any[]) => any + : ( + ...args: T[Uncapitalize] extends (...args: infer P) => any + ? P + : never + ) => any + : never + } + : {} + +export type ComponentInstance = InstanceType + +// public properties exposed on the proxy, which is used as the render context +// in templates (as `this` in the render option) +export type ComponentRenderProxy< + P = {}, // props type extracted from props option + B = {}, // raw bindings returned from setup() + D = {}, // return from data() + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + PublicProps = P, + Defaults = {}, + MakeDefaultsOptional extends boolean = false +> = { + $data: D + $props: Readonly< + MakeDefaultsOptional extends true + ? Partial & Omit

+ : P & PublicProps + > + $attrs: Record + $emit: ComponentRenderEmitFn< + Emits, + keyof Emits, + ComponentRenderProxy< + P, + B, + D, + C, + M, + Mixin, + Extends, + Emits, + PublicProps, + Defaults, + MakeDefaultsOptional + > + > +} & Readonly

& + ShallowUnwrapRef & + D & + M & + ExtractComputedReturns & + Omit + +// for Vetur and TSX support +type VueConstructorProxy< + PropsOptions, + RawBindings, + Data, + Computed extends ComputedOptions, + Methods extends MethodOptions, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + Props = ExtractPropTypes & + ({} extends Emits ? {} : EmitsToProps) +> = Omit & { + new (...args: any[]): ComponentRenderProxy< + Props, + ShallowUnwrapRef, + Data, + Computed, + Methods, + Mixin, + Extends, + Emits, + Props, + ExtractDefaultPropTypes, + true + > +} + +type DefaultData = object | ((this: V) => object) +type DefaultMethods = { [key: string]: (this: V, ...args: any[]) => any } +type DefaultComputed = { [key: string]: any } + +export type VueProxy< + PropsOptions, + RawBindings, + Data = DefaultData, + Computed extends ComputedOptions = DefaultComputed, + Methods extends MethodOptions = DefaultMethods, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {} +> = Vue2ComponentOptions< + Vue, + ShallowUnwrapRef & Data, + Methods, + Computed, + PropsOptions, + ExtractPropTypes +> & + VueConstructorProxy< + PropsOptions, + RawBindings, + Data, + Computed, + Methods, + Mixin, + Extends, + Emits + > + +// public properties exposed on the proxy, which is used as the render context +// in templates (as `this` in the render option) +export type ComponentPublicInstance< + P = {}, // props type extracted from props option + B = {}, // raw bindings returned from setup() + D = {}, // return from data() + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + E extends EmitsOptions = {}, + PublicProps = P, + Defaults = {}, + MakeDefaultsOptional extends boolean = false +> = { + $data: D + $props: MakeDefaultsOptional extends true + ? Partial & Omit

+ : P & PublicProps + $attrs: Data + $refs: Data + $slots: Slots + $root: ComponentPublicInstance | null + $parent: ComponentPublicInstance | null + $emit: EmitFn + $el: any + // $options: Options & MergedComponentOptionsOverride + $forceUpdate: () => void + $nextTick: typeof nextTick + $watch( + source: string | Function, + cb: Function, + options?: WatchOptions + ): WatchStopHandle +} & P & + ShallowUnwrapRef & + UnwrapNestedRefs & + ExtractComputedReturns & + M diff --git a/types/v3-define-component.d.ts b/types/v3-define-component.d.ts new file mode 100644 index 0000000000..8d3a47805c --- /dev/null +++ b/types/v3-define-component.d.ts @@ -0,0 +1,119 @@ +import { ComponentPropsOptions } from './v3-component-props' +import { + MethodOptions, + ComputedOptions, + ComponentOptionsWithoutProps, + ComponentOptionsWithArrayProps, + ComponentOptionsWithProps +} from './v3-component-options' +import { VueProxy } from './v3-component-proxy' +import { Data, HasDefined } from './common' +import { EmitsOptions } from './v3-setup-context' + +/** + * overload 1: object format with no props + */ +export function defineComponent< + RawBindings, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + EmitsNames extends string = string +>( + options: ComponentOptionsWithoutProps< + {}, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits, + EmitsNames + > +): VueProxy<{}, RawBindings, D, C, M, Mixin, Extends, Emits> +/** + * overload 2: object format with array props declaration + * props inferred as `{ [key in PropNames]?: any }` + * + * return type is for Vetur and TSX support + */ +export function defineComponent< + PropNames extends string, + RawBindings = Data, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + EmitsNames extends string = string, + PropsOptions extends ComponentPropsOptions = ComponentPropsOptions +>( + options: ComponentOptionsWithArrayProps< + PropNames, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits, + EmitsNames + > +): VueProxy< + Readonly<{ [key in PropNames]?: any }>, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits +> + +/** + * overload 3: object format with object props declaration + * + * see `ExtractPropTypes` in './componentProps.ts' + */ +export function defineComponent< + Props, + RawBindings = Data, + D = Data, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin = {}, + Extends = {}, + Emits extends EmitsOptions = {}, + EmitsNames extends string = string, + PropsOptions extends ComponentPropsOptions = ComponentPropsOptions +>( + options: HasDefined extends true + ? ComponentOptionsWithProps< + PropsOptions, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits, + EmitsNames, + Props + > + : ComponentOptionsWithProps< + PropsOptions, + RawBindings, + D, + C, + M, + Mixin, + Extends, + Emits, + EmitsNames + > +): VueProxy diff --git a/types/v3-directive.ts b/types/v3-directive.ts new file mode 100644 index 0000000000..f6b091f80e --- /dev/null +++ b/types/v3-directive.ts @@ -0,0 +1,29 @@ +import type { VNodeDirective, VNode } from './vnode' + +export type DirectiveModifiers = Record + +export interface DirectiveBinding extends Readonly { + readonly modifiers: DirectiveModifiers + readonly value: V + readonly oldValue: V | null +} + +export type DirectiveHook = ( + el: T, + binding: DirectiveBinding, + vnode: VNode, + prevVNode: Prev +) => void + +export interface ObjectDirective { + bind?: DirectiveHook + inserted?: DirectiveHook + update?: DirectiveHook + componentUpdated?: DirectiveHook + unbind?: DirectiveHook +} +export type FunctionDirective = DirectiveHook + +export type Directive = + | ObjectDirective + | FunctionDirective diff --git a/types/v3.d.ts b/types/v3-manual-apis.d.ts similarity index 54% rename from types/v3.d.ts rename to types/v3-manual-apis.d.ts index 464edd7f45..901494d624 100644 --- a/types/v3.d.ts +++ b/types/v3-manual-apis.d.ts @@ -1,12 +1,6 @@ -import { VNode } from './vnode' +import { SetupContext } from './v3-setup-context' import { CreateElement, Vue } from './vue' -export interface SetupContext { - attrs: Record - slots: Record VNode[]) | undefined> - emit: (event: string, ...args: any[]) => any -} - export function getCurrentInstance(): { proxy: Vue } | null export const h: CreateElement diff --git a/types/v3-setup-context.d.ts b/types/v3-setup-context.d.ts new file mode 100644 index 0000000000..43c412e67c --- /dev/null +++ b/types/v3-setup-context.d.ts @@ -0,0 +1,42 @@ +import { VNode } from './vnode' +import { Data, UnionToIntersection } from './common' +import { Vue } from './vue' + +export type Slot = (...args: any[]) => VNode[] + +export type Slots = Record + +export type ObjectEmitsOptions = Record< + string, + ((...args: any[]) => any) | null +> + +export type EmitsOptions = ObjectEmitsOptions | string[] + +export type EmitFn< + Options = ObjectEmitsOptions, + Event extends keyof Options = keyof Options, + ReturnType extends void | Vue = void +> = Options extends Array + ? (event: V, ...args: any[]) => ReturnType + : {} extends Options // if the emit is empty object (usually the default value for emit) should be converted to function + ? (event: string, ...args: any[]) => ReturnType + : UnionToIntersection< + { + [key in Event]: Options[key] extends (...args: infer Args) => any + ? (event: key, ...args: Args) => ReturnType + : (event: key, ...args: any[]) => ReturnType + }[Event] + > + +export type ComponentRenderEmitFn< + Options = ObjectEmitsOptions, + Event extends keyof Options = keyof Options, + T extends Vue | void = void +> = EmitFn + +export interface SetupContext { + attrs: Data + slots: Slots + emit: EmitFn +}