From a04385cd275ff6690a82a55b7f0b4d34ba7288b9 Mon Sep 17 00:00:00 2001 From: zhiyuanzmj <260480378@qq.com> Date: Tue, 10 Mar 2026 21:53:33 +0800 Subject: [PATCH 1/8] fix(runtime-core): prevent removing undefined event handlers in mergeProps Ensure event listeners that are explicitly null or undefined are preserved during prop merging instead of being dropped. --- packages/runtime-core/__tests__/vnode.spec.ts | 3 +++ packages/runtime-core/src/vnode.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/packages/runtime-core/__tests__/vnode.spec.ts b/packages/runtime-core/__tests__/vnode.spec.ts index aa9d4ff35db..23f30cd283f 100644 --- a/packages/runtime-core/__tests__/vnode.spec.ts +++ b/packages/runtime-core/__tests__/vnode.spec.ts @@ -472,6 +472,9 @@ describe('vnode', () => { expect(mergeProps(props1, props3)).toMatchObject({ onClick: clickHandler1, }) + expect(mergeProps({ onClick: null })).toMatchObject({ + onClick: null, + }) }) test('default', () => { diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index cd1ef948d73..35f9925d63e 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -891,6 +891,8 @@ export function mergeProps(...args: (Data & VNodeProps)[]): Data { ret[key] = existing ? [].concat(existing as any, incoming as any) : incoming + } else if (!incoming && !existing) { + ret[key] = incoming } } else if (key !== '') { ret[key] = toMerge[key] From b69cf4d6d3ff0d19a7f2e8bfeb4d9bfb33b24893 Mon Sep 17 00:00:00 2001 From: zhiyuanzmj <260480378@qq.com> Date: Tue, 7 Apr 2026 00:10:47 +0800 Subject: [PATCH 2/8] feat(types): support object Slots type for defineComponent --- .../dts-test/defineComponent.test-d.tsx | 71 +++++++++++++++++++ .../runtime-core/src/apiDefineComponent.ts | 14 ++-- packages/runtime-core/src/component.ts | 4 +- .../src/componentPublicInstance.ts | 6 +- 4 files changed, 83 insertions(+), 12 deletions(-) diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index 6188b102b31..e6f42afe64b 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -1175,6 +1175,77 @@ describe('componentOptions setup should be `SetupContext`', () => { ) }) +describe('infer slots from `SetupContext`', () => { + // options + const Foo = defineComponent({ + setup( + _props, + _ctx: SetupContext< + EmitsOptions, + { + default: (props: { foo: number }) => any + } + >, + ) {}, + render() { + this.$slots.default({ foo: 1 }) + }, + }) + const foo = {} as InstanceType + expectType>(false) + foo.$slots.default({ foo: 1 }) + + const Bar = defineComponent({ + setup( + _props, + _ctx: SetupContext< + EmitsOptions, + { + default: (props: { foo: number }) => any + } + >, + ) {}, + render() { + this.$slots.default({ foo: 1 }) + }, + }) + const bar = {} as InstanceType + expectType>(false) + bar.$slots.default({ foo: 1 }) + + // functional + const Baz = defineComponent( + ( + _props: { foo: T }, + _ctx: { + slots: { + default: (props: { foo: T }) => any + } + }, + ) => + () => [], + ) + const baz = new Baz({ foo: 1 }) + expectType>(false) + baz.$slots.default?.({ foo: 1 }) + + const Qux = defineComponent( + ( + _props: { foo: T }, + _ctx: SetupContext< + EmitsOptions, + { + default: (props: { foo: T }) => any + } + >, + ) => + () => [], + ) + const qux = new Qux({ foo: 1 }) + expectType>(false) + qux.$slots.default?.({ foo: 1 }) +}) + describe('extract instance type', () => { const Base = defineComponent({ props: { diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 4865c3b4ea4..e17216cef28 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -33,7 +33,7 @@ import type { ComponentPublicInstanceConstructor, CreateComponentPublicInstanceWithMixins, } from './componentPublicInstance' -import type { SlotsType } from './componentSlots' +import type { Slots, SlotsType } from './componentSlots' import type { Directive } from './directives' import type { ComponentTypeEmits } from './apiSetupHelpers' @@ -150,7 +150,7 @@ export function defineComponent< Props extends Record, E extends EmitsOptions = {}, EE extends string = string, - S extends SlotsType = {}, + S extends SlotsType | Slots = {}, >( setup: ( props: Props, @@ -166,7 +166,7 @@ export function defineComponent< Props extends Record, E extends EmitsOptions = {}, EE extends string = string, - S extends SlotsType = {}, + S extends SlotsType | Slots = {}, >( setup: ( props: Props, @@ -199,7 +199,7 @@ export function defineComponent< Extends extends ComponentOptionsMixin = ComponentOptionsMixin, InjectOptions extends ComponentInjectOptions = {}, InjectKeys extends string = string, - Slots extends SlotsType = {}, + TypeSlots extends SlotsType | Slots = {}, LocalComponents extends Record = {}, Directives extends Record = {}, Exposed extends string = string, @@ -249,7 +249,7 @@ export function defineComponent< {}, // Defaults InjectOptions, InjectKeys, - Slots, + TypeSlots, LocalComponents, Directives, Exposed, @@ -269,7 +269,7 @@ export function defineComponent< {}, false, InjectOptions, - Slots, + TypeSlots, LocalComponents, Directives, string @@ -288,7 +288,7 @@ export function defineComponent< PublicProps, ToResolvedProps, ExtractDefaultPropTypes, - Slots, + TypeSlots, LocalComponents, Directives, Exposed, diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index c46741bee80..ec6e399418d 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -287,11 +287,11 @@ export type LifecycleHook = (TFn & SchedulerJob)[] | null // use `E extends any` to force evaluating type to fix #2362 export type SetupContext< E = EmitsOptions, - S extends SlotsType = {}, + S extends SlotsType | Slots = {}, > = E extends any ? { attrs: Attrs - slots: UnwrapSlotsType + slots: S extends SlotsType ? UnwrapSlotsType : Readonly emit: EmitFn expose: = Record>( exposed?: Exposed, diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 8d3fdb056a9..3add965863f 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -50,7 +50,7 @@ import { shouldCacheAccess, } from './componentOptions' import type { EmitFn, EmitsOptions } from './componentEmits' -import type { SlotsType, UnwrapSlotsType } from './componentSlots' +import type { Slots, SlotsType, UnwrapSlotsType } from './componentSlots' import { markAttrsAccessed } from './componentRenderUtils' import { currentRenderingInstance } from './componentRenderContext' import { warn } from './warning' @@ -224,7 +224,7 @@ export type CreateComponentPublicInstanceWithMixins< Defaults = {}, MakeDefaultsOptional extends boolean = false, I extends ComponentInjectOptions = {}, - S extends SlotsType = {}, + S extends SlotsType | Slots = {}, LC extends Record = {}, Directives extends Record = {}, Exposed extends string = string, @@ -272,7 +272,7 @@ export type CreateComponentPublicInstanceWithMixins< Provide >, I, - S, + S extends SlotsType ? S : SlotsType, Exposed, TypeRefs, TypeEl From 16762ce011798141637997c9e154058ff0082bf4 Mon Sep 17 00:00:00 2001 From: zhiyuanzmj <260480378@qq.com> Date: Tue, 7 Apr 2026 00:21:13 +0800 Subject: [PATCH 3/8] chore: update --- .../dts-test/defineComponent.test-d.tsx | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index e6f42afe64b..5c3fe71da83 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -1198,12 +1198,11 @@ describe('infer slots from `SetupContext`', () => { const Bar = defineComponent({ setup( _props, - _ctx: SetupContext< - EmitsOptions, - { + _ctx: { + slots: { default: (props: { foo: number }) => any } - >, + }, ) {}, render() { this.$slots.default({ foo: 1 }) @@ -1217,11 +1216,12 @@ describe('infer slots from `SetupContext`', () => { const Baz = defineComponent( ( _props: { foo: T }, - _ctx: { - slots: { + _ctx: SetupContext< + EmitsOptions, + { default: (props: { foo: T }) => any } - }, + >, ) => () => [], ) @@ -1232,12 +1232,11 @@ describe('infer slots from `SetupContext`', () => { const Qux = defineComponent( ( _props: { foo: T }, - _ctx: SetupContext< - EmitsOptions, - { + _ctx: { + slots: { default: (props: { foo: T }) => any } - >, + }, ) => () => [], ) From 6dc9ed7d6cb65b3d3a02b4484a0ec6903c1186e0 Mon Sep 17 00:00:00 2001 From: zhiyuanzmj <260480378@qq.com> Date: Tue, 7 Apr 2026 00:24:34 +0800 Subject: [PATCH 4/8] chore: update --- packages-private/dts-test/defineComponent.test-d.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index 5c3fe71da83..93aea41b641 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -1227,7 +1227,7 @@ describe('infer slots from `SetupContext`', () => { ) const baz = new Baz({ foo: 1 }) expectType>(false) - baz.$slots.default?.({ foo: 1 }) + baz.$slots.default({ foo: 1 }) const Qux = defineComponent( ( @@ -1242,7 +1242,7 @@ describe('infer slots from `SetupContext`', () => { ) const qux = new Qux({ foo: 1 }) expectType>(false) - qux.$slots.default?.({ foo: 1 }) + qux.$slots.default({ foo: 1 }) }) describe('extract instance type', () => { From b98f262653c8cd158697c2b847fad6d4ea624e26 Mon Sep 17 00:00:00 2001 From: zhiyuanzmj <260480378@qq.com> Date: Tue, 7 Apr 2026 00:42:40 +0800 Subject: [PATCH 5/8] chore: update --- packages-private/dts-test/defineComponent.test-d.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index 93aea41b641..9e341a5831c 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -2,6 +2,7 @@ import { type Component, type ComponentOptions, type ComponentPublicInstance, + type EmitsOptions, type PropType, type SetupContext, type Slots, @@ -1820,7 +1821,6 @@ import type { ComponentOptionsMixin, DefineComponent, Directive, - EmitsOptions, ExtractPropTypes, KeepAliveProps, TransitionProps, From 94a719b825042445fa141539363f0ca5d4d9d147 Mon Sep 17 00:00:00 2001 From: zhiyuanzmj <260480378@qq.com> Date: Wed, 8 Apr 2026 23:20:14 +0800 Subject: [PATCH 6/8] chore: update --- packages-private/dts-test/defineComponent.test-d.tsx | 8 ++++---- packages/runtime-core/src/apiDefineComponent.ts | 8 ++++---- packages/runtime-core/src/component.ts | 2 +- packages/runtime-core/src/componentPublicInstance.ts | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index 9e341a5831c..b5188405b30 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -1184,7 +1184,7 @@ describe('infer slots from `SetupContext`', () => { _ctx: SetupContext< EmitsOptions, { - default: (props: { foo: number }) => any + default: (props: { foo: number }) => VNode } >, ) {}, @@ -1201,7 +1201,7 @@ describe('infer slots from `SetupContext`', () => { _props, _ctx: { slots: { - default: (props: { foo: number }) => any + default: (props: { foo: number }) => VNode } }, ) {}, @@ -1220,7 +1220,7 @@ describe('infer slots from `SetupContext`', () => { _ctx: SetupContext< EmitsOptions, { - default: (props: { foo: T }) => any + default: (props: { foo: T }) => VNode } >, ) => @@ -1235,7 +1235,7 @@ describe('infer slots from `SetupContext`', () => { _props: { foo: T }, _ctx: { slots: { - default: (props: { foo: T }) => any + default: (props: { foo: T }) => VNode } }, ) => diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index e17216cef28..6dd1f89214e 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -33,7 +33,7 @@ import type { ComponentPublicInstanceConstructor, CreateComponentPublicInstanceWithMixins, } from './componentPublicInstance' -import type { Slots, SlotsType } from './componentSlots' +import type { SlotsType } from './componentSlots' import type { Directive } from './directives' import type { ComponentTypeEmits } from './apiSetupHelpers' @@ -150,7 +150,7 @@ export function defineComponent< Props extends Record, E extends EmitsOptions = {}, EE extends string = string, - S extends SlotsType | Slots = {}, + S extends SlotsType | Record = {}, >( setup: ( props: Props, @@ -166,7 +166,7 @@ export function defineComponent< Props extends Record, E extends EmitsOptions = {}, EE extends string = string, - S extends SlotsType | Slots = {}, + S extends SlotsType | Record = {}, >( setup: ( props: Props, @@ -199,7 +199,7 @@ export function defineComponent< Extends extends ComponentOptionsMixin = ComponentOptionsMixin, InjectOptions extends ComponentInjectOptions = {}, InjectKeys extends string = string, - TypeSlots extends SlotsType | Slots = {}, + TypeSlots extends SlotsType | Record = {}, LocalComponents extends Record = {}, Directives extends Record = {}, Exposed extends string = string, diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index ec6e399418d..93847406d8f 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -287,7 +287,7 @@ export type LifecycleHook = (TFn & SchedulerJob)[] | null // use `E extends any` to force evaluating type to fix #2362 export type SetupContext< E = EmitsOptions, - S extends SlotsType | Slots = {}, + S extends SlotsType | Record = {}, > = E extends any ? { attrs: Attrs diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 3add965863f..68e9fd794c4 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -50,7 +50,7 @@ import { shouldCacheAccess, } from './componentOptions' import type { EmitFn, EmitsOptions } from './componentEmits' -import type { Slots, SlotsType, UnwrapSlotsType } from './componentSlots' +import type { SlotsType, UnwrapSlotsType } from './componentSlots' import { markAttrsAccessed } from './componentRenderUtils' import { currentRenderingInstance } from './componentRenderContext' import { warn } from './warning' @@ -224,7 +224,7 @@ export type CreateComponentPublicInstanceWithMixins< Defaults = {}, MakeDefaultsOptional extends boolean = false, I extends ComponentInjectOptions = {}, - S extends SlotsType | Slots = {}, + S extends SlotsType | Record = {}, LC extends Record = {}, Directives extends Record = {}, Exposed extends string = string, From 3c921991bc24a7983078aabb1489b050002defc6 Mon Sep 17 00:00:00 2001 From: zhiyuanzmj <260480378@qq.com> Date: Wed, 8 Apr 2026 23:23:55 +0800 Subject: [PATCH 7/8] chore: update --- packages/runtime-core/src/apiDefineComponent.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 6dd1f89214e..b18daf49dde 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -199,7 +199,7 @@ export function defineComponent< Extends extends ComponentOptionsMixin = ComponentOptionsMixin, InjectOptions extends ComponentInjectOptions = {}, InjectKeys extends string = string, - TypeSlots extends SlotsType | Record = {}, + Slots extends SlotsType | Record = {}, LocalComponents extends Record = {}, Directives extends Record = {}, Exposed extends string = string, @@ -249,7 +249,7 @@ export function defineComponent< {}, // Defaults InjectOptions, InjectKeys, - TypeSlots, + Slots, LocalComponents, Directives, Exposed, @@ -269,7 +269,7 @@ export function defineComponent< {}, false, InjectOptions, - TypeSlots, + Slots, LocalComponents, Directives, string @@ -288,7 +288,7 @@ export function defineComponent< PublicProps, ToResolvedProps, ExtractDefaultPropTypes, - TypeSlots, + Slots, LocalComponents, Directives, Exposed, From d937991803f3b90eb49b476ff13e2d4dc72749ea Mon Sep 17 00:00:00 2001 From: zhiyuanzmj <260480378@qq.com> Date: Thu, 9 Apr 2026 08:29:59 +0800 Subject: [PATCH 8/8] chore: update test --- .../dts-test/defineComponent.test-d.tsx | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index b5188405b30..084bd44dcba 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -1181,12 +1181,7 @@ describe('infer slots from `SetupContext`', () => { const Foo = defineComponent({ setup( _props, - _ctx: SetupContext< - EmitsOptions, - { - default: (props: { foo: number }) => VNode - } - >, + _ctx: SetupContext, ) {}, render() { this.$slots.default({ foo: 1 }) @@ -1216,15 +1211,16 @@ describe('infer slots from `SetupContext`', () => { // functional const Baz = defineComponent( ( - _props: { foo: T }, - _ctx: SetupContext< + props: { foo: T }, + ctx: SetupContext< EmitsOptions, - { + SlotsType<{ default: (props: { foo: T }) => VNode - } + }> >, ) => - () => [], + () => + ctx.slots.default(props), ) const baz = new Baz({ foo: 1 }) expectType>(false) @@ -1232,14 +1228,15 @@ describe('infer slots from `SetupContext`', () => { const Qux = defineComponent( ( - _props: { foo: T }, - _ctx: { + props: { foo: T }, + ctx: { slots: { default: (props: { foo: T }) => VNode } }, ) => - () => [], + () => + ctx.slots.default(props), ) const qux = new Qux({ foo: 1 }) expectType>(false)