diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineAttrs.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineAttrs.spec.ts.snap new file mode 100644 index 00000000000..80d066e8a67 --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineAttrs.spec.ts.snap @@ -0,0 +1,46 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`defineAttrs() > basic usage 1`] = ` +"import { useAttrs as _useAttrs, defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + setup(__props, { expose: __expose }) { + __expose(); + + const attrs = _useAttrs() + +return { attrs } +} + +})" +`; + +exports[`defineAttrs() > w/o generic params 1`] = ` +"import { useAttrs as _useAttrs } from 'vue' + +export default { + setup(__props, { expose: __expose }) { + __expose(); + + const attrs = _useAttrs() + +return { attrs } +} + +}" +`; + +exports[`defineAttrs() > w/o return value 1`] = ` +"import { defineComponent as _defineComponent } from 'vue' + +export default /*#__PURE__*/_defineComponent({ + setup(__props, { expose: __expose }) { + __expose(); + + + +return { } +} + +})" +`; diff --git a/packages/compiler-sfc/__tests__/compileScript/defineAttrs.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineAttrs.spec.ts new file mode 100644 index 00000000000..e155e3c6bfd --- /dev/null +++ b/packages/compiler-sfc/__tests__/compileScript/defineAttrs.spec.ts @@ -0,0 +1,40 @@ +import { compileSFCScript as compile, assertCode } from '../utils' + +describe('defineAttrs()', () => { + test('basic usage', () => { + const { content } = compile(` + <script setup lang="ts"> + const attrs = defineAttrs<{ + bar?: number + }>() + </script> + `) + assertCode(content) + expect(content).toMatch(`const attrs = _useAttrs()`) + expect(content).not.toMatch('defineAttrs') + }) + + test('w/o return value', () => { + const { content } = compile(` + <script setup lang="ts"> + defineAttrs<{ + bar?: number + }>() + </script> + `) + assertCode(content) + expect(content).not.toMatch('defineAttrs') + expect(content).not.toMatch(`_useAttrs`) + }) + + test('w/o generic params', () => { + const { content } = compile(` + <script setup> + const attrs = defineAttrs() + </script> + `) + assertCode(content) + expect(content).toMatch(`const attrs = _useAttrs()`) + expect(content).not.toMatch('defineAttrs') + }) +}) diff --git a/packages/compiler-sfc/src/compileScript.ts b/packages/compiler-sfc/src/compileScript.ts index 2a33f69936d..f5c902f3df9 100644 --- a/packages/compiler-sfc/src/compileScript.ts +++ b/packages/compiler-sfc/src/compileScript.ts @@ -53,6 +53,7 @@ import { import { analyzeScriptBindings } from './script/analyzeScriptBindings' import { isImportUsed } from './script/importUsageCheck' import { processAwait } from './script/topLevelAwait' +import { processDefineAttrs } from './script/defineAttrs' export interface SFCScriptCompileOptions { /** @@ -512,7 +513,8 @@ export function compileScript( processDefineProps(ctx, expr) || processDefineEmits(ctx, expr) || processDefineOptions(ctx, expr) || - processDefineSlots(ctx, expr) + processDefineSlots(ctx, expr) || + processDefineAttrs(ctx, expr) ) { ctx.s.remove(node.start! + startOffset, node.end! + startOffset) } else if (processDefineExpose(ctx, expr)) { @@ -550,7 +552,8 @@ export function compileScript( !isDefineProps && processDefineEmits(ctx, init, decl.id) !isDefineEmits && (processDefineSlots(ctx, init, decl.id) || - processDefineModel(ctx, init, decl.id)) + processDefineModel(ctx, init, decl.id) || + processDefineAttrs(ctx, init, decl.id)) if ( isDefineProps && diff --git a/packages/compiler-sfc/src/script/context.ts b/packages/compiler-sfc/src/script/context.ts index 692eab3ab9e..0d1607ab46d 100644 --- a/packages/compiler-sfc/src/script/context.ts +++ b/packages/compiler-sfc/src/script/context.ts @@ -36,6 +36,7 @@ export class ScriptCompileContext { hasDefineOptionsCall = false hasDefineSlotsCall = false hasDefineModelCall = false + hasDefineAttrsCall = false // defineProps propsCall: CallExpression | undefined diff --git a/packages/compiler-sfc/src/script/defineAttrs.ts b/packages/compiler-sfc/src/script/defineAttrs.ts new file mode 100644 index 00000000000..d2eda8d9962 --- /dev/null +++ b/packages/compiler-sfc/src/script/defineAttrs.ts @@ -0,0 +1,33 @@ +import { LVal, Node } from '@babel/types' +import { isCallOf } from './utils' +import { ScriptCompileContext } from './context' + +export const DEFINE_ATTRS = 'defineAttrs' + +export function processDefineAttrs( + ctx: ScriptCompileContext, + node: Node, + declId?: LVal +): boolean { + if (!isCallOf(node, DEFINE_ATTRS)) { + return false + } + if (ctx.hasDefineAttrsCall) { + ctx.error(`duplicate ${DEFINE_ATTRS}() call`, node) + } + ctx.hasDefineAttrsCall = true + + if (node.arguments.length > 0) { + ctx.error(`${DEFINE_ATTRS}() cannot accept arguments`, node) + } + + if (declId) { + ctx.s.overwrite( + ctx.startOffset! + node.start!, + ctx.startOffset! + node.end!, + `${ctx.helper('useAttrs')}()` + ) + } + + return true +} diff --git a/packages/dts-test/defineComponent.test-d.tsx b/packages/dts-test/defineComponent.test-d.tsx index 7466249e10f..7a9e9737785 100644 --- a/packages/dts-test/defineComponent.test-d.tsx +++ b/packages/dts-test/defineComponent.test-d.tsx @@ -10,10 +10,13 @@ import { SetupContext, h, SlotsType, + AttrsType, Slots, - VNode + VNode, + ImgHTMLAttributes, + StyleValue } from 'vue' -import { describe, expectType, IsUnion } from './utils' +import { describe, expectType, IsUnion, test } from './utils' describe('with object props', () => { interface ExpectedProps { @@ -1188,6 +1191,270 @@ describe('async setup', () => { vm.a = 2 }) +describe('define attrs', () => { + test('define attrs w/ object props', () => { + const MyComp = defineComponent({ + props: { + foo: String + }, + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType<number | undefined>(this.$attrs.bar) + } + }) + expectType<JSX.Element>(<MyComp foo="1" bar={1} />) + }) + + test('define attrs w/ array props', () => { + const MyComp = defineComponent({ + props: ['foo'], + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType<number | undefined>(this.$attrs.bar) + } + }) + expectType<JSX.Element>(<MyComp foo="1" bar={1} />) + }) + + test('define attrs w/ no props', () => { + const MyComp = defineComponent({ + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType<number | undefined>(this.$attrs.bar) + } + }) + expectType<JSX.Element>(<MyComp bar={1} />) + }) + + 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<string>(props.foo) + expectType<number | undefined>(attrs.bar) + } + }) + expectType<JSX.Element>(<MyComp foo="1" bar={1} />) + }) + + test('define attrs w/ functional component', () => { + const MyComp = defineComponent( + (props: { foo: string }, ctx) => { + expectType<number | undefined>(ctx.attrs.bar) + return () => ( + // return a render function (both JSX and h() works) + <div>{props.foo}</div> + ) + }, + { + attrs: Object as AttrsType<{ + bar?: number + }> + } + ) + expectType<JSX.Element>(<MyComp foo={'1'} bar={1} />) + }) + + 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<string | undefined>(this.foo) + } + }) + expectType<JSX.Element>(<MyComp foo="1" />) + }) + + test('define required attrs', () => { + const MyComp = defineComponent({ + attrs: Object as AttrsType<{ + bar: number + }>, + created() { + expectType<number | undefined>(this.$attrs.bar) + } + }) + expectType<JSX.Element>(<MyComp bar={1} />) + // @ts-expect-error + expectType<JSX.Element>(<MyComp />) + }) + + test('define no attrs w/ object props', () => { + const MyComp = defineComponent({ + props: { + foo: String + }, + created() { + expectType<unknown>(this.$attrs.bar) + } + }) + // @ts-expect-error + expectType<JSX.Element>(<MyComp foo="1" bar={1} />) + }) + + test('define no attrs w/ functional component', () => { + const MyComp = defineComponent((props: { foo: string }, ctx) => { + expectType<unknown>(ctx.attrs.bar) + return () => ( + // return a render function (both JSX and h() works) + <div>{props.foo}</div> + ) + }) + expectType<JSX.Element>(<MyComp foo={'1'} />) + // @ts-expect-error + expectType<JSX.Element>(<MyComp foo="1" bar={1} />) + }) + test('wrap elements', () => { + const MyImg = defineComponent({ + props: { + foo: String + }, + attrs: Object as AttrsType<ImgHTMLAttributes>, + created() { + expectType<any>(this.$attrs.class) + expectType<StyleValue | undefined>(this.$attrs.style) + }, + render() { + return <img {...this.$attrs} /> + } + }) + expectType<JSX.Element>(<MyImg class={'str'} style={'str'} src={'str'} />) + }) + + test('wrap components', () => { + const Child = defineComponent({ + props: { + foo: String + }, + emits: { + baz: (val: number) => true + }, + render() { + return <div>{this.foo}</div> + } + }) + const Comp = defineComponent({ + props: { + bar: Number + }, + attrs: Object as AttrsType<typeof Child>, + created() { + expectType<unknown>(this.$attrs.class) + expectType<unknown>(this.$attrs.style) + }, + render() { + return <Child {...this.$attrs} /> + } + }) + expectType<JSX.Element>( + <Comp + class={'str'} + style={'str'} + bar={1} + foo={'str'} + onBaz={val => { + expectType<number>(val) + }} + /> + ) + }) + + test('wrap components w/ functional component', () => { + const Child = defineComponent((props: { foo: string }, ctx) => { + return () => <div>{props.foo}</div> + }) + const Comp = defineComponent({ + props: { + bar: Number + }, + attrs: Object as AttrsType<typeof Child>, + created() { + expectType<unknown>(this.$attrs.class) + expectType<unknown>(this.$attrs.style) + }, + render() { + return <Child {...this.$attrs} /> + } + }) + expectType<JSX.Element>( + <Comp class={'str'} style={'str'} bar={1} foo={'str'} /> + ) + }) + test('ignore reserved props', () => { + defineComponent({ + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + // @ts-expect-error reserved props + this.$attrs.key + // @ts-expect-error reserved props + this.$attrs.ref + } + }) + defineComponent( + (props: { foo: string }, ctx) => { + // @ts-expect-error reserved props + ctx.attrs.key + // @ts-expect-error reserved props + ctx.attrs.ref + return () => <div>{props.foo}</div> + }, + { + attrs: Object as AttrsType<{ + bar?: number + }> + } + ) + }) + + test('always readonly', () => { + defineComponent({ + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + // @ts-expect-error readonly + this.$attrs.class = 'test' + } + }) + defineComponent( + (props: { foo: string }, ctx) => { + // @ts-expect-error readonly + ctx.attrs.bar = 1 + return () => <div>{props.foo}</div> + }, + { + attrs: Object as AttrsType<{ + bar?: number + }> + } + ) + }) +}) + // #5948 describe('DefineComponent should infer correct types when assigning to Component', () => { let component: Component diff --git a/packages/dts-test/defineCustomElement.test-d.ts b/packages/dts-test/defineCustomElement.test-d.ts index 4e7cf228372..5d56a65c0c8 100644 --- a/packages/dts-test/defineCustomElement.test-d.ts +++ b/packages/dts-test/defineCustomElement.test-d.ts @@ -1,5 +1,5 @@ -import { defineCustomElement } from 'vue' -import { expectType, describe } from './utils' +import { defineCustomElement, AttrsType } from 'vue' +import { describe, expectType, test } from './utils' describe('inject', () => { // with object inject @@ -62,3 +62,96 @@ describe('inject', () => { } }) }) + +describe('define attrs', () => { + test('define attrs w/ object props', () => { + defineCustomElement({ + props: { + foo: String + }, + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType<number | undefined>(this.$attrs.bar) + } + }) + }) + + test('define attrs w/ array props', () => { + defineCustomElement({ + props: ['foo'], + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType<number | undefined>(this.$attrs.bar) + } + }) + }) + + test('define attrs w/ no props', () => { + defineCustomElement({ + attrs: Object as AttrsType<{ + bar?: number + }>, + created() { + expectType<number | undefined>(this.$attrs.bar) + } + }) + }) + + test('define attrs as low priority', () => { + defineCustomElement({ + props: { + foo: String + }, + attrs: Object as AttrsType<{ + foo: number + }>, + created() { + // @ts-expect-error + this.$attrs.foo + expectType<string | undefined>(this.foo) + } + }) + }) + + test('define attrs w/ no attrs', () => { + defineCustomElement({ + props: { + foo: String + }, + created() { + expectType<unknown>(this.$attrs.bar) + expectType<unknown>(this.$attrs.baz) + } + }) + }) + + test('default attrs like class, style', () => { + defineCustomElement({ + props: { + foo: String + }, + created() { + expectType<unknown>(this.$attrs.class) + expectType<unknown>(this.$attrs.style) + } + }) + }) + + test('define required attrs', () => { + defineCustomElement({ + props: { + foo: String + }, + attrs: Object as AttrsType<{ + bar: number + }>, + created() { + expectType<number>(this.$attrs.bar) + } + }) + }) +}) diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 272bb548751..66f8de4b65b 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -8,12 +8,15 @@ import { RenderFunction, ComponentOptionsBase, ComponentInjectOptions, - ComponentOptions + ComponentOptions, + AttrsType, + UnwrapAttrsType } from './componentOptions' import { SetupContext, AllowedComponentProps, - ComponentCustomProps + ComponentCustomProps, + HasDefinedAttrs } from './component' import { ExtractPropTypes, @@ -54,7 +57,8 @@ export type DefineComponent< PP = PublicProps, Props = ResolveProps<PropsOrPropOptions, E>, Defaults = ExtractDefaultPropTypes<PropsOrPropOptions>, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown> > = ComponentPublicInstanceConstructor< CreateComponentPublicInstance< Props, @@ -69,7 +73,8 @@ export type DefineComponent< Defaults, true, {}, - S + S, + Attrs > & Props > & @@ -86,7 +91,8 @@ export type DefineComponent< Defaults, {}, string, - S + S, + Attrs > & PP @@ -101,18 +107,23 @@ export function defineComponent< Props extends Record<string, any>, E extends EmitsOptions = {}, EE extends string = string, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown>, + PropsAttrs = HasDefinedAttrs<Attrs> extends true + ? UnwrapAttrsType<NonNullable<Attrs>> + : {} >( setup: ( props: Props, - ctx: SetupContext<E, S> + ctx: SetupContext<E, S, Attrs> ) => RenderFunction | Promise<RenderFunction>, options?: Pick<ComponentOptions, 'name' | 'inheritAttrs'> & { props?: (keyof Props)[] emits?: E | EE[] slots?: S + attrs?: Attrs } -): (props: Props & EmitsToProps<E>) => any +): (props: Props & EmitsToProps<E> & PropsAttrs) => any export function defineComponent< Props extends Record<string, any>, E extends EmitsOptions = {}, @@ -144,6 +155,7 @@ export function defineComponent< E extends EmitsOptions = {}, EE extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown>, I extends ComponentInjectOptions = {}, II extends string = string >( @@ -159,7 +171,8 @@ export function defineComponent< EE, I, II, - S + S, + Attrs > ): DefineComponent< Props, @@ -174,7 +187,8 @@ export function defineComponent< PublicProps, ResolveProps<Props, E>, ExtractDefaultPropTypes<Props>, - S + S, + Attrs > // overload 3: object format with array props declaration @@ -191,6 +205,7 @@ export function defineComponent< E extends EmitsOptions = {}, EE extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown>, I extends ComponentInjectOptions = {}, II extends string = string, Props = Readonly<{ [key in PropNames]?: any }> @@ -207,7 +222,8 @@ export function defineComponent< EE, I, II, - S + S, + Attrs > ): DefineComponent< Props, @@ -222,7 +238,8 @@ export function defineComponent< PublicProps, ResolveProps<Props, E>, ExtractDefaultPropTypes<Props>, - S + S, + Attrs > // overload 4: object format with object props declaration @@ -241,7 +258,8 @@ export function defineComponent< EE extends string = string, S extends SlotsType = {}, I extends ComponentInjectOptions = {}, - II extends string = string + II extends string = string, + Attrs extends AttrsType = Record<string, unknown> >( options: ComponentOptionsWithObjectProps< PropsOptions, @@ -255,7 +273,8 @@ export function defineComponent< EE, I, II, - S + S, + Attrs > ): DefineComponent< PropsOptions, @@ -270,7 +289,8 @@ export function defineComponent< PublicProps, ResolveProps<PropsOptions, E>, ExtractDefaultPropTypes<PropsOptions>, - S + S, + Attrs > // implementation, close to no-op diff --git a/packages/runtime-core/src/apiSetupHelpers.ts b/packages/runtime-core/src/apiSetupHelpers.ts index 93200667081..b673d20e7f8 100644 --- a/packages/runtime-core/src/apiSetupHelpers.ts +++ b/packages/runtime-core/src/apiSetupHelpers.ts @@ -18,7 +18,8 @@ import { ComponentOptionsMixin, ComponentOptionsWithoutProps, ComputedOptions, - MethodOptions + MethodOptions, + StrictUnwrapAttrsType } from './componentOptions' import { ComponentPropsOptions, @@ -215,6 +216,15 @@ export function defineSlots< return null as any } +export function defineAttrs< + Attrs extends Record<string, any> = Record<string, any> +>(): StrictUnwrapAttrsType<Attrs> { + if (__DEV__) { + warnRuntimeUsage(`defineAttrs`) + } + return null as any +} + /** * (**Experimental**) Vue `<script setup>` compiler macro for declaring a * two-way binding prop that can be consumed via `v-model` from the parent diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 57a53a39b76..491d3f9b0f8 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -40,10 +40,12 @@ import { AppContext, createAppContext, AppConfig } from './apiCreateApp' import { Directive, validateDirectiveName } from './directives' import { applyOptions, + AttrsType, ComponentOptions, ComputedOptions, MethodOptions, - resolveMergedOptions + resolveMergedOptions, + UnwrapAttrsType } from './componentOptions' import { EmitsOptions, @@ -64,7 +66,8 @@ import { ShapeFlags, extend, getGlobalThis, - IfAny + IfAny, + Equal } from '@vue/shared' import { SuspenseBoundary } from './components/Suspense' import { CompilerOptions } from '@vue/compiler-core' @@ -81,7 +84,10 @@ import { SchedulerJob } from './scheduler' import { LifecycleHooks } from './enums' export type Data = Record<string, unknown> - +// Whether the attrs option is defined +export type HasDefinedAttrs<T> = Equal<keyof T, string> extends true + ? false + : true /** * For extending allowed non-declared props on components in TSX */ @@ -184,10 +190,13 @@ type LifecycleHook<TFn = Function> = TFn[] | null // use `E extends any` to force evaluating type to fix #2362 export type SetupContext< E = EmitsOptions, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown> > = E extends any ? { - attrs: Data + attrs: HasDefinedAttrs<Attrs> extends true + ? UnwrapAttrsType<NonNullable<Attrs>> + : Data slots: UnwrapSlotsType<S> emit: EmitFn<E> expose: (exposed?: Record<string, any>) => void diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index de4d304448a..95d7bb6ddef 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -111,7 +111,8 @@ export interface ComponentOptionsBase< Defaults = {}, I extends ComponentInjectOptions = {}, II extends string = string, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown> > extends LegacyOptions<Props, D, C, M, Mixin, Extends, I, II>, ComponentInternalOptions, ComponentCustomOptions { @@ -126,7 +127,7 @@ export interface ComponentOptionsBase< > > >, - ctx: SetupContext<E, S> + ctx: SetupContext<E, S, Attrs> ) => Promise<RawBindings> | RawBindings | RenderFunction | void name?: string template?: string | object // can be a direct DOM node @@ -141,6 +142,7 @@ export interface ComponentOptionsBase< inheritAttrs?: boolean emits?: (E | EE[]) & ThisType<void> slots?: S + attrs?: Attrs // TODO infer public instance type based on exposed keys expose?: string[] serverPrefetch?(): void | Promise<any> @@ -224,6 +226,7 @@ export type ComponentOptionsWithoutProps< I extends ComponentInjectOptions = {}, II extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown>, PE = Props & EmitsToProps<E> > = ComponentOptionsBase< PE, @@ -238,7 +241,8 @@ export type ComponentOptionsWithoutProps< {}, I, II, - S + S, + Attrs > & { props?: undefined } & ThisType< @@ -255,7 +259,8 @@ export type ComponentOptionsWithoutProps< {}, false, I, - S + S, + Attrs > > @@ -272,6 +277,7 @@ export type ComponentOptionsWithArrayProps< I extends ComponentInjectOptions = {}, II extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown>, Props = Prettify<Readonly<{ [key in PropNames]?: any } & EmitsToProps<E>>> > = ComponentOptionsBase< Props, @@ -286,7 +292,8 @@ export type ComponentOptionsWithArrayProps< {}, I, II, - S + S, + Attrs > & { props: PropNames[] } & ThisType< @@ -303,7 +310,8 @@ export type ComponentOptionsWithArrayProps< {}, false, I, - S + S, + Attrs > > @@ -320,6 +328,7 @@ export type ComponentOptionsWithObjectProps< I extends ComponentInjectOptions = {}, II extends string = string, S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown>, Props = Prettify<Readonly<ExtractPropTypes<PropsOptions> & EmitsToProps<E>>>, Defaults = ExtractDefaultPropTypes<PropsOptions> > = ComponentOptionsBase< @@ -335,7 +344,8 @@ export type ComponentOptionsWithObjectProps< Defaults, I, II, - S + S, + Attrs > & { props: PropsOptions & ThisType<void> } & ThisType< @@ -352,7 +362,8 @@ export type ComponentOptionsWithObjectProps< Defaults, false, I, - S + S, + Attrs > > @@ -406,6 +417,25 @@ export type ComponentOptionsMixin = ComponentOptionsBase< any > +declare const AttrSymbol: unique symbol +export type AttrsType<T extends Record<string, any> = Record<string, any>> = { + [AttrSymbol]?: T extends new () => { $props: infer P } + ? NonNullable<P> + : T extends (props: infer P, ...args: any) => any + ? P + : T +} + +export type UnwrapAttrsType< + Attrs extends AttrsType, + T = NonNullable<Attrs[typeof AttrSymbol]> +> = [keyof Attrs] extends [never] + ? Data + : Readonly< + Prettify<{ + [K in keyof T]: T[K] + }> + > export type ComputedOptions = Record< string, ComputedGetter<any> | WritableComputedOptions<any> @@ -423,6 +453,17 @@ export type ExtractComputedReturns<T extends any> = { : never } +export type StrictUnwrapAttrsType< + Attrs extends Record<string, unknown>, + T = NonNullable<Attrs> +> = [keyof T] extends [never] + ? Readonly<Record<string, unknown>> + : T extends new () => { $props: infer P } + ? Readonly<NonNullable<P>> + : T extends (props: infer P, ...args: any) => any + ? Readonly<NonNullable<P>> + : Readonly<T> + export type ObjectWatchOptionItem = { handler: WatchCallback | string } & WatchOptions diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 7b552c8f92a..16fc1ce7abb 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -1,6 +1,8 @@ import { + AllowedComponentProps, ComponentInternalInstance, Data, + HasDefinedAttrs, getExposeProxy, isStatefulComponent } from './component' @@ -37,7 +39,9 @@ import { shouldCacheAccess, MergedComponentOptionsOverride, InjectToObject, - ComponentInjectOptions + ComponentInjectOptions, + AttrsType, + UnwrapAttrsType } from './componentOptions' import { EmitsOptions, EmitFn } from './componentEmits' import { SlotsType, UnwrapSlotsType } from './componentSlots' @@ -149,6 +153,7 @@ export type CreateComponentPublicInstance< MakeDefaultsOptional extends boolean = false, I extends ComponentInjectOptions = {}, S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown>, PublicMixin = IntersectionMixin<Mixin> & IntersectionMixin<Extends>, PublicP = UnwrapMixinsType<PublicMixin, 'P'> & EnsureNonVoid<P>, PublicB = UnwrapMixinsType<PublicMixin, 'B'> & EnsureNonVoid<B>, @@ -182,10 +187,12 @@ export type CreateComponentPublicInstance< Defaults, {}, string, - S + S, + Attrs >, I, - S + S, + Attrs > // public properties exposed on the proxy, which is used as the render context @@ -202,14 +209,23 @@ export type ComponentPublicInstance< MakeDefaultsOptional extends boolean = false, Options = ComponentOptionsBase<any, any, any, any, any, any, any, any, any>, I extends ComponentInjectOptions = {}, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown>, // Attrs type extracted from attrs option + // AttrsProps type used for JSX validation of attrs + AttrsProps = HasDefinedAttrs<Attrs> extends true // if attrs is defined + ? Omit<UnwrapAttrsType<Attrs>, keyof (P & PublicProps)> // exclude props from attrs, for JSX validation + : {} // no JSX validation of attrs > = { $: ComponentInternalInstance $data: D $props: MakeDefaultsOptional extends true - ? Partial<Defaults> & Omit<Prettify<P> & PublicProps, keyof Defaults> - : Prettify<P> & PublicProps - $attrs: Data + ? Partial<Defaults> & + Omit<Prettify<P> & PublicProps, keyof Defaults> & + AttrsProps + : Prettify<P> & PublicProps & AttrsProps + $attrs: HasDefinedAttrs<Attrs> extends true + ? Readonly<AttrsProps & AllowedComponentProps> + : Data $refs: Data $slots: UnwrapSlotsType<S> $root: ComponentPublicInstance | null diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 85bd92e75b0..8cec27eb510 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -71,6 +71,7 @@ export { defineExpose, defineOptions, defineSlots, + defineAttrs, defineModel, withDefaults, useModel @@ -246,7 +247,8 @@ export type { MethodOptions, ComputedOptions, RuntimeCompilerOptions, - ComponentInjectOptions + ComponentInjectOptions, + AttrsType } from './componentOptions' export type { EmitsOptions, ObjectEmitsOptions } from './componentEmits' export type { diff --git a/packages/runtime-core/types/scriptSetupHelpers.d.ts b/packages/runtime-core/types/scriptSetupHelpers.d.ts index a9ba734a53d..9224ebb9a5b 100644 --- a/packages/runtime-core/types/scriptSetupHelpers.d.ts +++ b/packages/runtime-core/types/scriptSetupHelpers.d.ts @@ -7,6 +7,7 @@ type _defineOptions = typeof defineOptions type _defineSlots = typeof defineSlots type _defineModel = typeof defineModel type _withDefaults = typeof withDefaults +type _defineAttrs = typeof defineAttrs declare global { const defineProps: _defineProps @@ -16,4 +17,5 @@ declare global { const defineSlots: _defineSlots const defineModel: _defineModel const withDefaults: _withDefaults + const defineAttrs: _defineAttrs } diff --git a/packages/runtime-dom/__tests__/customElement.spec.ts b/packages/runtime-dom/__tests__/customElement.spec.ts index 6ccf02c4fa6..ea1af5e594c 100644 --- a/packages/runtime-dom/__tests__/customElement.spec.ts +++ b/packages/runtime-dom/__tests__/customElement.spec.ts @@ -1,4 +1,5 @@ import { + AttrsType, defineAsyncComponent, defineComponent, defineCustomElement, @@ -287,8 +288,9 @@ describe('defineCustomElement', () => { describe('attrs', () => { const E = defineCustomElement({ + attrs: Object as AttrsType<{ foo: string }>, render() { - return [h('div', null, this.$attrs.foo as string)] + return [h('div', null, this.$attrs.foo)] } }) customElements.define('my-el-attrs', E) diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 5662b0b535b..48be004f437 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -21,7 +21,8 @@ import { ConcreteComponent, ComponentOptions, ComponentInjectOptions, - SlotsType + SlotsType, + AttrsType } from '@vue/runtime-core' import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared' import { hydrate, render } from '.' @@ -54,7 +55,8 @@ export function defineCustomElement< EE extends string = string, I extends ComponentInjectOptions = {}, II extends string = string, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown> >( options: ComponentOptionsWithoutProps< Props, @@ -68,7 +70,8 @@ export function defineCustomElement< EE, I, II, - S + S, + Attrs > & { styles?: string[] } ): VueElementConstructor<Props> @@ -85,7 +88,8 @@ export function defineCustomElement< EE extends string = string, I extends ComponentInjectOptions = {}, II extends string = string, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown> >( options: ComponentOptionsWithArrayProps< PropNames, @@ -99,7 +103,8 @@ export function defineCustomElement< EE, I, II, - S + S, + Attrs > & { styles?: string[] } ): VueElementConstructor<{ [K in PropNames]: any }> @@ -116,7 +121,8 @@ export function defineCustomElement< EE extends string = string, I extends ComponentInjectOptions = {}, II extends string = string, - S extends SlotsType = {} + S extends SlotsType = {}, + Attrs extends AttrsType = Record<string, unknown> >( options: ComponentOptionsWithObjectProps< PropsOptions, @@ -130,7 +136,8 @@ export function defineCustomElement< EE, I, II, - S + S, + Attrs > & { styles?: string[] } ): VueElementConstructor<ExtractPropTypes<PropsOptions>> diff --git a/packages/shared/src/typeUtils.ts b/packages/shared/src/typeUtils.ts index 1deb4729125..92790d9ec8f 100644 --- a/packages/shared/src/typeUtils.ts +++ b/packages/shared/src/typeUtils.ts @@ -21,3 +21,11 @@ export type Awaited<T> = T extends null | undefined ? Awaited<V> // recursively unwrap the value : never // the argument to `then` was not callable : T // non-object or non-thenable + +// Conditional returns can enforce identical types. +// See here: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650 +export type Equal<Left, Right> = (<U>() => U extends Left ? 1 : 0) extends < + U +>() => U extends Right ? 1 : 0 + ? true + : false