From 071a17570c7e504fa4f76294766c64048b546da1 Mon Sep 17 00:00:00 2001 From: rudyxu1102 Date: Mon, 25 Sep 2023 19:59:17 +0800 Subject: [PATCH] feat(types): support inferring attrs --- types/index.d.ts | 3 +- types/options.d.ts | 8 +- types/test/v3/define-component-test.tsx | 200 +++++++++++++++++++++++- types/umd.d.ts | 3 +- types/v3-component-options.d.ts | 55 +++++-- types/v3-component-public-instance.d.ts | 25 ++- types/v3-define-component.d.ts | 61 ++++++-- types/v3-setup-context.d.ts | 7 +- types/vue.d.ts | 14 +- 9 files changed, 326 insertions(+), 50 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 19fce98726..be711ef07d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -49,7 +49,8 @@ export { ComputedOptions as ComponentComputedOptions, MethodOptions as ComponentMethodOptions, ComponentPropsOptions, - ComponentCustomOptions + ComponentCustomOptions, + AttrsType } from './v3-component-options' export { ComponentInstance, diff --git a/types/options.d.ts b/types/options.d.ts index 19c339f745..eef74c860a 100644 --- a/types/options.d.ts +++ b/types/options.d.ts @@ -3,8 +3,9 @@ import { VNode, VNodeData, VNodeDirective, NormalizedScopedSlot } from './vnode' import { SetupContext } from './v3-setup-context' import { DebuggerEvent } from './v3-generated' import { DefineComponent } from './v3-define-component' -import { ComponentOptionsMixin } from './v3-component-options' +import { AttrsType, ComponentOptionsMixin, noAttrsDefine, UnwrapAttrsType } from './v3-component-options' import { ObjectDirective, FunctionDirective } from './v3-directive' +import { Data } from './common' type Constructor = { new (...args: any[]): any @@ -263,7 +264,7 @@ export interface FunctionalComponentOptions< ): VNode | VNode[] } -export interface RenderContext { +export interface RenderContext> { props: Props children: VNode[] slots(): any @@ -272,6 +273,9 @@ export interface RenderContext { listeners: { [key: string]: Function | Function[] } scopedSlots: { [key: string]: NormalizedScopedSlot } injections: any + attrs: noAttrsDefine extends true + ? Data + : UnwrapAttrsType> } export type Prop = diff --git a/types/test/v3/define-component-test.tsx b/types/test/v3/define-component-test.tsx index 7e6d1968ca..c924cc3172 100644 --- a/types/test/v3/define-component-test.tsx +++ b/types/test/v3/define-component-test.tsx @@ -1,13 +1,14 @@ -import Vue, { VueConstructor } from '../../index' +import Vue, { AttrsType, ExtractPropTypes, VueConstructor } from '../../index' import { Component, defineComponent, PropType, - ref, - reactive, ComponentPublicInstance } from '../../index' +import { reactive } from '../../../src/v3/reactivity/reactive' +import { ref } from '../../../src/v3/reactivity/ref' import { describe, test, expectType, expectError, IsUnion } from '../utils' +import { ImgHTMLAttributes, StyleValue } from '../../jsx' describe('compat with v2 APIs', () => { const comp = defineComponent({}) @@ -1079,7 +1080,9 @@ export default { } }) } - +const Foo = defineComponent((props: { msg: string }) => { + return () =>
{props.msg}
+}) describe('functional w/ array props', () => { const Foo = defineComponent({ functional: true, @@ -1225,3 +1228,192 @@ describe('should report non-existent properties in instance', () => { // @ts-expect-error instance2.foo }) +const MyComp = defineComponent({ + props: { + foo: String + }, + + created() { + console.log(this) + } +}) +describe('define attrs', () => { + test('define attrs w/ object props', () => { + const MyComp = defineComponent({ + props: { + foo: String + }, + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + expectType() + }) + + test('define attrs w/ array props', () => { + const MyComp = defineComponent({ + props: ['foo'], + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + expectType() + }) + + test('define attrs w/ no props', () => { + const MyComp = defineComponent({ + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + expectType() + }) + + test('define attrs w/ composition api', () => { + const MyComp = defineComponent({ + props: { + foo: { + type: String, + required: true + } + }, + attrs: Object as AttrsType<{ + bar?: number + }>, + setup(props, { attrs }) { + expectType(props.foo) + expectType(attrs.bar) + } + }) + expectType() + }) + + test('functional w/ array props', () => { + const Comp = defineComponent({ + functional: true, + props: ['foo'], + attrs: Object as AttrsType<{ + bar?: number + }>, + render(h, ctx) { + expectType(ctx.props.foo) + expectType(ctx.attrs.bar) + } + }); + + + }) + + test('functional w/ object props', () => { + const Comp = defineComponent({ + functional: true, + props: { + foo: String + }, + attrs: Object as AttrsType<{ + bar?: number + }>, + render(h, ctx) { + expectType(ctx.props.foo) + expectType(ctx.attrs.bar) + } + }); + + }) + + test('define attrs as low priority', () => { + const MyComp = defineComponent({ + props: { + foo: String + }, + attrs: Object as AttrsType<{ + foo?: number + }>, + created() { + // @ts-expect-error + this.$attrs.foo + + expectType(this.foo) + } + }) + expectType() + }) + + test('define required attrs', () => { + const MyComp = defineComponent({ + attrs: Object as AttrsType<{ + bar: number + }>, + created() { + expectType(this.$attrs.bar) + } + }) + expectType() + // @ts-expect-error + expectType() + }) + + test('define no attrs w/ object props', () => { + const MyComp = defineComponent({ + props: { + foo: String + }, + created() { + expectType(this.$attrs.bar) + } + }) + // @ts-expect-error + expectType() + }) + + test('wrap elements, such as img element', () => { + const MyImg = defineComponent({ + props: { + foo: String + }, + attrs: Object as AttrsType, + created() { + expectType(this.$attrs.class) + expectType(this.$attrs.style) + }, + render() { + return + } + }) + expectType() + }) + + test('secondary packaging of components', () => { + const childProps = { + foo: String + } + type ChildProps = ExtractPropTypes + const Child = defineComponent({ + props: childProps, + render() { + return
{this.foo}
+ } + }) + const Comp = defineComponent({ + props: { + bar: Number + }, + attrs: Object as AttrsType, + render() { + return + } + }) + expectType( + + ) + }) +}) diff --git a/types/umd.d.ts b/types/umd.d.ts index 3df7afef21..300fd558cb 100644 --- a/types/umd.d.ts +++ b/types/umd.d.ts @@ -1,4 +1,5 @@ import * as V from './index' +import { AttrsType } from './index' import { DefaultData, DefaultProps, @@ -38,7 +39,7 @@ declare namespace Vue { Props = DefaultProps, PropDefs = PropsDefinition > = V.FunctionalComponentOptions - export type RenderContext = V.RenderContext + export type RenderContext> = V.RenderContext export type PropType = V.PropType export type PropOptions = V.PropOptions export type ComputedOptions = V.ComputedOptions diff --git a/types/v3-component-options.d.ts b/types/v3-component-options.d.ts index e2da34e753..28f3fabe0d 100644 --- a/types/v3-component-options.d.ts +++ b/types/v3-component-options.d.ts @@ -2,7 +2,7 @@ import { Vue } from './vue' import { VNode } from './vnode' import { ComponentOptions as Vue2ComponentOptions } from './options' import { EmitsOptions, SetupContext } from './v3-setup-context' -import { Data, LooseRequired, UnionToIntersection } from './common' +import { Data, Equal, LooseRequired, UnionToIntersection } from './common' import { ComponentPropsOptions, ExtractDefaultPropTypes, @@ -37,6 +37,9 @@ export interface WritableComputedOptions { set: ComputedSetter } +// Whether the attrs option is not defined +export type noAttrsDefine = Equal + export type ComputedOptions = Record< string, ComputedGetter | WritableComputedOptions @@ -49,11 +52,12 @@ export interface MethodOptions { export type SetupFunction< Props, RawBindings = {}, - Emits extends EmitsOptions = {} + Emits extends EmitsOptions = {}, + Attrs extends AttrsType = Record > = ( this: void, props: Readonly, - ctx: SetupContext + ctx: SetupContext ) => RawBindings | (() => VNode | null) | void type ExtractOptionProp = T extends ComponentOptionsBase< @@ -82,7 +86,8 @@ export interface ComponentOptionsBase< Extends extends ComponentOptionsMixin, Emits extends EmitsOptions, EmitNames extends string = string, - Defaults = {} + Defaults = {}, + Attrs extends AttrsType = Record > extends Omit< Vue2ComponentOptions, 'data' | 'computed' | 'methods' | 'setup' | 'props' | 'mixins' | 'extends' @@ -101,6 +106,7 @@ export interface ComponentOptionsBase< mixins?: Mixin[] extends?: Extends emits?: (Emits | EmitNames[]) & ThisType + attrs?: Attrs setup?: SetupFunction< Readonly< LooseRequired< @@ -110,7 +116,8 @@ export interface ComponentOptionsBase< > >, RawBindings, - Emits + Emits, + Attrs > __defaults?: Defaults @@ -147,6 +154,7 @@ export type ComponentOptionsWithProps< Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNames extends string = string, + Attrs extends AttrsType = Record, Props = ExtractPropTypes, Defaults = ExtractDefaultPropTypes > = ComponentOptionsBase< @@ -159,7 +167,8 @@ export type ComponentOptionsWithProps< Extends, Emits, EmitsNames, - Defaults + Defaults, + Attrs > & { props?: PropsOptions } & ThisType< @@ -171,7 +180,8 @@ export type ComponentOptionsWithProps< M, Mixin, Extends, - Emits + Emits, + Attrs > > @@ -185,6 +195,7 @@ export type ComponentOptionsWithArrayProps< Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNames extends string = string, + Attrs extends AttrsType = Record, Props = Readonly<{ [key in PropNames]?: any }> > = ComponentOptionsBase< Props, @@ -196,7 +207,8 @@ export type ComponentOptionsWithArrayProps< Extends, Emits, EmitsNames, - {} + {}, + Attrs > & { props?: PropNames[] } & ThisType< @@ -208,7 +220,8 @@ export type ComponentOptionsWithArrayProps< M, Mixin, Extends, - Emits + Emits, + Attrs > > @@ -221,7 +234,8 @@ export type ComponentOptionsWithoutProps< Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, - EmitsNames extends string = string + EmitsNames extends string = string, + Attrs extends AttrsType = Record > = ComponentOptionsBase< Props, RawBindings, @@ -232,7 +246,8 @@ export type ComponentOptionsWithoutProps< Extends, Emits, EmitsNames, - {} + {}, + Attrs > & { props?: undefined } & ThisType< @@ -244,9 +259,25 @@ export type ComponentOptionsWithoutProps< M, Mixin, Extends, - Emits + Emits, + Attrs > > export type WithLegacyAPI = T & Omit, keyof T> + + +declare const AttrSymbol: unique symbol +export type AttrsType = Record> = { + [AttrSymbol]?: T +} + +export type UnwrapAttrsType< + Attrs extends AttrsType, + T = NonNullable +> = [keyof Attrs] extends [never] + ? Data + : Readonly<{ + [K in keyof T]: T[K] + }> diff --git a/types/v3-component-public-instance.d.ts b/types/v3-component-public-instance.d.ts index 1c55908ac7..ea88844d99 100644 --- a/types/v3-component-public-instance.d.ts +++ b/types/v3-component-public-instance.d.ts @@ -1,8 +1,12 @@ import { - DebuggerEvent, ShallowUnwrapRef, +} from '../src/v3/reactivity/ref' +import { UnwrapNestedRefs -} from './v3-generated' +} from '../src/v3/reactivity/reactive' +import { + DebuggerEvent +} from '../src/v3/debug' import { UnionToIntersection } from './common' import { Vue, VueConstructor } from './vue' @@ -11,7 +15,8 @@ import { MethodOptions, ExtractComputedReturns, ComponentOptionsMixin, - ComponentOptionsBase + ComponentOptionsBase, + AttrsType } from './v3-component-options' import { EmitFn, EmitsOptions } from './v3-setup-context' @@ -99,6 +104,7 @@ export type CreateComponentPublicInstance< Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = {}, + Attrs extends AttrsType = Record, PublicProps = P, Defaults = {}, MakeDefaultsOptional extends boolean = false, @@ -121,7 +127,8 @@ export type CreateComponentPublicInstance< E, PublicProps, PublicDefaults, - MakeDefaultsOptional + MakeDefaultsOptional, + Attrs > // public properties exposed on the proxy, which is used as the render context @@ -136,6 +143,7 @@ export type ComponentPublicInstance< PublicProps = P, Defaults = {}, MakeDefaultsOptional extends boolean = false, + Attrs extends AttrsType = Record, Options = ComponentOptionsBase< any, any, @@ -155,7 +163,8 @@ export type ComponentPublicInstance< E, Defaults, MakeDefaultsOptional, - Options + Options, + Attrs > & Readonly

& ShallowUnwrapRef & @@ -171,7 +180,8 @@ interface Vue3Instance< E, Defaults, MakeDefaultsOptional, - Options + Options, + Attrs extends AttrsType > extends Vue< D, Readonly< @@ -181,7 +191,8 @@ interface Vue3Instance< >, ComponentPublicInstance, Options & MergedComponentOptionsOverride, - EmitFn + EmitFn, + Attrs > {} type MergedHook void> = T | T[] diff --git a/types/v3-define-component.d.ts b/types/v3-define-component.d.ts index 03ef52d185..2fd370e241 100644 --- a/types/v3-define-component.d.ts +++ b/types/v3-define-component.d.ts @@ -10,7 +10,10 @@ import { ComponentOptionsWithArrayProps, ComponentOptionsWithProps, ComponentOptionsMixin, - ComponentOptionsBase + ComponentOptionsBase, + AttrsType, + noAttrsDefine, + UnwrapAttrsType } from './v3-component-options' import { ComponentPublicInstanceConstructor, @@ -30,6 +33,7 @@ export type DefineComponent< Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = {}, EE extends string = string, + Attrs extends AttrsType = Record, Props = Readonly< PropsOrPropOptions extends ComponentPropsOptions ? ExtractPropTypes @@ -46,6 +50,7 @@ export type DefineComponent< Mixin, Extends, E, + Attrs, Props, Defaults, true @@ -62,7 +67,8 @@ export type DefineComponent< Extends, E, EE, - Defaults + Defaults, + Attrs > & { props: PropsOrPropOptions } @@ -78,7 +84,8 @@ export function defineComponent< Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, - EmitsNames extends string = string + EmitsNames extends string = string, + Attrs extends AttrsType = Record >( options: { functional?: never } & ComponentOptionsWithoutProps< {}, @@ -89,9 +96,10 @@ export function defineComponent< Mixin, Extends, Emits, - EmitsNames + EmitsNames, + Attrs > -): DefineComponent<{}, RawBindings, D, C, M, Mixin, Extends, Emits> +): DefineComponent<{}, RawBindings, D, C, M, Mixin, Extends, Emits, EmitsNames, Attrs> /** * overload 2: object format with array props declaration @@ -109,7 +117,8 @@ export function defineComponent< Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNames extends string = string, - PropsOptions extends ComponentPropsOptions = ComponentPropsOptions + PropsOptions extends ComponentPropsOptions = ComponentPropsOptions, + Attrs extends AttrsType = Record >( options: { functional?: never } & ComponentOptionsWithArrayProps< PropNames, @@ -120,7 +129,8 @@ export function defineComponent< Mixin, Extends, Emits, - EmitsNames + EmitsNames, + Attrs > ): DefineComponent< Readonly<{ [key in PropNames]?: any }>, @@ -130,7 +140,9 @@ export function defineComponent< M, Mixin, Extends, - Emits + Emits, + EmitsNames, + Attrs > /** @@ -148,7 +160,8 @@ export function defineComponent< Extends extends ComponentOptionsMixin = ComponentOptionsMixin, Emits extends EmitsOptions = {}, EmitsNames extends string = string, - PropsOptions extends ComponentPropsOptions = ComponentPropsOptions + PropsOptions extends ComponentPropsOptions = ComponentPropsOptions, + Attrs extends AttrsType = Record >( options: HasDefined extends true ? { functional?: never } & ComponentOptionsWithProps< @@ -161,6 +174,7 @@ export function defineComponent< Extends, Emits, EmitsNames, + Attrs, Props > : { functional?: never } & ComponentOptionsWithProps< @@ -172,30 +186,43 @@ export function defineComponent< Mixin, Extends, Emits, - EmitsNames + EmitsNames, + Attrs > -): DefineComponent +): DefineComponent /** * overload 4.1: functional component with array props */ export function defineComponent< PropNames extends string, - Props = Readonly<{ [key in PropNames]?: any }> + Props = Readonly<{ [key in PropNames]?: any }>, + Attrs extends AttrsType = Record, + // AttrsProps type used for JSX validation of attrs + AttrsProps = noAttrsDefine extends true // if attrs is not defined + ? {} // no JSX validation of attrs + : Omit, keyof Props> // exclude props from attrs, for JSX validation >(options: { functional: true props?: PropNames[] - render?: (h: CreateElement, context: RenderContext) => any -}): DefineComponent + attrs?: Attrs, + render?: (h: CreateElement, context: RenderContext) => any +}): DefineComponent /** * overload 4.2: functional component with object props */ export function defineComponent< PropsOptions extends ComponentPropsOptions = ComponentPropsOptions, - Props = ExtractPropTypes + Props = ExtractPropTypes, + Attrs extends AttrsType = Record, + // AttrsProps type used for JSX validation of attrs + AttrsProps = noAttrsDefine extends true // if attrs is not defined + ? {} // no JSX validation of attrs + : Omit, keyof Props> // exclude props from attrs, for JSX validation >(options: { functional: true props?: PropsOptions - render?: (h: CreateElement, context: RenderContext) => any -}): DefineComponent + attrs?: Attrs, + render?: (h: CreateElement, context: RenderContext) => any +}): DefineComponent diff --git a/types/v3-setup-context.d.ts b/types/v3-setup-context.d.ts index 77b49bed8a..c8be1687cc 100644 --- a/types/v3-setup-context.d.ts +++ b/types/v3-setup-context.d.ts @@ -1,6 +1,7 @@ import { VNode } from './vnode' import { Data, UnionToIntersection } from './common' import { Vue } from './vue' +import { AttrsType, noAttrsDefine, UnwrapAttrsType } from './v3-component-options' export type Slot = (...args: any[]) => VNode[] @@ -29,8 +30,10 @@ export type EmitFn< }[Event] > -export interface SetupContext { - attrs: Data +export interface SetupContext> { + attrs: noAttrsDefine extends true + ? Data + : UnwrapAttrsType> /** * Equivalent of `this.$listeners`, which is Vue 2 only. */ diff --git a/types/vue.d.ts b/types/vue.d.ts index f158cf0171..903e864e54 100644 --- a/types/vue.d.ts +++ b/types/vue.d.ts @@ -20,7 +20,10 @@ import { } from './v3-component-public-instance' import { ExtractComputedReturns, - ComponentOptionsMixin + ComponentOptionsMixin, + UnwrapAttrsType, + AttrsType, + noAttrsDefine } from './v3-component-options' import { Directive, ObjectDirective } from './v3-directive' @@ -51,11 +54,12 @@ export interface Vue< Props = Record, Instance = never, Options = never, - Emit = (event: string, ...args: any[]) => Vue + Emit = (event: string, ...args: any[]) => Vue, + Attrs extends AttrsType = Record > { // properties with different types in defineComponent() readonly $data: Data - readonly $props: Props + readonly $props: noAttrsDefine extends true ? Props : Props & Omit, keyof Props> readonly $parent: NeverFallback | null readonly $root: NeverFallback readonly $children: NeverFallback[] @@ -78,7 +82,9 @@ export interface Vue< readonly $ssrContext: any readonly $vnode: VNode - readonly $attrs: Record + readonly $attrs: noAttrsDefine extends true + ? Record + : Omit, keyof Props> readonly $listeners: Record $mount(elementOrSelector?: Element | string, hydrating?: boolean): this