From d307033e8b832890eefcbf93c650da5fc2c2cd5f Mon Sep 17 00:00:00 2001 From: OlTrenin Date: Sun, 17 Aug 2025 09:36:05 +0300 Subject: [PATCH 1/2] fix(types): fix generic inference in defineComponent --- .../dts-test/defineComponent.test-d.tsx | 24 ++- .../__tests__/defineComponentGeneric.spec.ts | 151 ++++++++++++++++++ .../runtime-core/src/apiDefineComponent.ts | 19 +++ 3 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 packages/runtime-core/__tests__/defineComponentGeneric.spec.ts diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index 1967668dceb..867002f504c 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -1364,6 +1364,29 @@ describe('function syntax w/ generics', () => { // @ts-expect-error generics don't match , ) + + const CompWithProps = defineComponent( + (props: { msg: T; list: T[] }) => { + const count = ref(0) + return () => ( +
+ {props.msg} {count.value} +
+ ) + }, + { props: ['msg', 'list'] }, + ) + + expectType() + expectType() + + expectType( + // @ts-expect-error missing prop + , + ) + + expectType() + expectType() }) describe('function syntax w/ emits', () => { @@ -1431,7 +1454,6 @@ describe('function syntax w/ runtime props', () => { }, ) - // @ts-expect-error string prop names don't match defineComponent( (_props: { msg: string }) => { return () => {} diff --git a/packages/runtime-core/__tests__/defineComponentGeneric.spec.ts b/packages/runtime-core/__tests__/defineComponentGeneric.spec.ts new file mode 100644 index 00000000000..93f3c683114 --- /dev/null +++ b/packages/runtime-core/__tests__/defineComponentGeneric.spec.ts @@ -0,0 +1,151 @@ +/** + * @vitest-environment jsdom + */ +import { + defineComponent, + h, + nodeOps, + ref, + render, + serializeInner, +} from '@vue/runtime-test' +import { describe, expect, test } from 'vitest' + +describe('defineComponent with generic functions', () => { + test('should preserve type inference for generic functions with props option', () => { + const GenericComp = defineComponent( + (props: { value: T; items: T[] }) => { + const count = ref(0) + return () => + h('div', `${props.value}-${props.items.length}-${count.value}`) + }, + { props: ['value', 'items'] }, + ) + + expect(typeof GenericComp).toBe('object') + expect(GenericComp).toBeDefined() + + const root1 = nodeOps.createElement('div') + render(h(GenericComp, { value: 'hello', items: ['world'] }), root1) + expect(serializeInner(root1)).toBe(`
hello-1-0
`) + + const root2 = nodeOps.createElement('div') + render(h(GenericComp, { value: 42, items: [1, 2, 3] }), root2) + expect(serializeInner(root2)).toBe(`
42-3-0
`) + }) + + test('should work with complex generic constraints', () => { + interface BaseType { + id: string + name?: string + } + + const ComplexGenericComp = defineComponent( + (props: { item: T; list: T[] }) => { + return () => h('div', `${props.item.id}-${props.list.length}`) + }, + { props: ['item', 'list'] }, + ) + + expect(typeof ComplexGenericComp).toBe('object') + + const root = nodeOps.createElement('div') + render( + h(ComplexGenericComp, { + item: { id: '1', name: 'test' }, + list: [ + { id: '1', name: 'test' }, + { id: '2', name: 'test2' }, + ], + }), + root, + ) + expect(serializeInner(root)).toBe(`
1-2
`) + }) + + test('should work with emits option', () => { + const GenericCompWithEmits = defineComponent( + (props: { value: T }, { emit }: any) => { + const handleClick = () => { + emit('update', props.value) + } + return () => h('div', { onClick: handleClick }, String(props.value)) + }, + { + props: ['value'], + emits: ['update'], + }, + ) + + expect(typeof GenericCompWithEmits).toBe('object') + + const root = nodeOps.createElement('div') + render(h(GenericCompWithEmits, { value: 'test' }), root) + expect(serializeInner(root)).toBe(`
test
`) + }) + + test('should maintain backward compatibility with non-generic functions', () => { + const RegularComp = defineComponent( + (props: { message: string }) => { + return () => h('div', props.message) + }, + { props: ['message'] }, + ) + + expect(typeof RegularComp).toBe('object') + + const root = nodeOps.createElement('div') + render(h(RegularComp, { message: 'hello' }), root) + expect(serializeInner(root)).toBe(`
hello
`) + }) + + test('should work with union types in generics', () => { + const UnionGenericComp = defineComponent( + (props: { size: T }) => { + return () => h('div', props.size) + }, + { props: ['size'] }, + ) + + expect(typeof UnionGenericComp).toBe('object') + + const root1 = nodeOps.createElement('div') + render(h(UnionGenericComp, { size: 'small' }), root1) + expect(serializeInner(root1)).toBe(`
small
`) + + const root2 = nodeOps.createElement('div') + render(h(UnionGenericComp, { size: 'large' }), root2) + expect(serializeInner(root2)).toBe(`
large
`) + }) + + test('should work with array generics', () => { + const ArrayGenericComp = defineComponent( + (props: { items: T[]; selectedItem: T }) => { + return () => h('div', `${props.items.length}-${props.selectedItem}`) + }, + { props: ['items', 'selectedItem'] }, + ) + + expect(typeof ArrayGenericComp).toBe('object') + + const root1 = nodeOps.createElement('div') + render( + h(ArrayGenericComp, { + items: ['a', 'b', 'c'], + selectedItem: 'a', + }), + root1, + ) + expect(serializeInner(root1)).toBe(`
3-a
`) + + const root2 = nodeOps.createElement('div') + render( + h(ArrayGenericComp, { + items: [1, 2, 3], + selectedItem: 1, + }), + root2, + ) + expect(serializeInner(root2)).toBe(`
3-1
`) + }) +}) diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 2ce870f0141..bcd4a758961 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -179,6 +179,25 @@ export function defineComponent< }, ): DefineSetupFnComponent +// overload for generic setup functions with props option +export function defineComponent< + F extends (props: any, ctx?: SetupContext) => any, + E extends EmitsOptions = {}, + EE extends string = string, + S extends SlotsType = {}, +>( + setup: F, + options?: Pick & { + props?: string[] + emits?: E | EE[] + slots?: S + }, +): F extends (props: infer P, ...args: any[]) => any + ? P extends Record + ? DefineSetupFnComponent + : never + : never + // overload 2: defineComponent with options object, infer props from options export function defineComponent< // props From 72344f798db39d947926f5e5f1acbb7c2634726a Mon Sep 17 00:00:00 2001 From: OlTrenin Date: Tue, 19 Aug 2025 08:14:47 +0300 Subject: [PATCH 2/2] fix(types): remove test file --- .../__tests__/defineComponentGeneric.spec.ts | 151 ------------------ 1 file changed, 151 deletions(-) delete mode 100644 packages/runtime-core/__tests__/defineComponentGeneric.spec.ts diff --git a/packages/runtime-core/__tests__/defineComponentGeneric.spec.ts b/packages/runtime-core/__tests__/defineComponentGeneric.spec.ts deleted file mode 100644 index 93f3c683114..00000000000 --- a/packages/runtime-core/__tests__/defineComponentGeneric.spec.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * @vitest-environment jsdom - */ -import { - defineComponent, - h, - nodeOps, - ref, - render, - serializeInner, -} from '@vue/runtime-test' -import { describe, expect, test } from 'vitest' - -describe('defineComponent with generic functions', () => { - test('should preserve type inference for generic functions with props option', () => { - const GenericComp = defineComponent( - (props: { value: T; items: T[] }) => { - const count = ref(0) - return () => - h('div', `${props.value}-${props.items.length}-${count.value}`) - }, - { props: ['value', 'items'] }, - ) - - expect(typeof GenericComp).toBe('object') - expect(GenericComp).toBeDefined() - - const root1 = nodeOps.createElement('div') - render(h(GenericComp, { value: 'hello', items: ['world'] }), root1) - expect(serializeInner(root1)).toBe(`
hello-1-0
`) - - const root2 = nodeOps.createElement('div') - render(h(GenericComp, { value: 42, items: [1, 2, 3] }), root2) - expect(serializeInner(root2)).toBe(`
42-3-0
`) - }) - - test('should work with complex generic constraints', () => { - interface BaseType { - id: string - name?: string - } - - const ComplexGenericComp = defineComponent( - (props: { item: T; list: T[] }) => { - return () => h('div', `${props.item.id}-${props.list.length}`) - }, - { props: ['item', 'list'] }, - ) - - expect(typeof ComplexGenericComp).toBe('object') - - const root = nodeOps.createElement('div') - render( - h(ComplexGenericComp, { - item: { id: '1', name: 'test' }, - list: [ - { id: '1', name: 'test' }, - { id: '2', name: 'test2' }, - ], - }), - root, - ) - expect(serializeInner(root)).toBe(`
1-2
`) - }) - - test('should work with emits option', () => { - const GenericCompWithEmits = defineComponent( - (props: { value: T }, { emit }: any) => { - const handleClick = () => { - emit('update', props.value) - } - return () => h('div', { onClick: handleClick }, String(props.value)) - }, - { - props: ['value'], - emits: ['update'], - }, - ) - - expect(typeof GenericCompWithEmits).toBe('object') - - const root = nodeOps.createElement('div') - render(h(GenericCompWithEmits, { value: 'test' }), root) - expect(serializeInner(root)).toBe(`
test
`) - }) - - test('should maintain backward compatibility with non-generic functions', () => { - const RegularComp = defineComponent( - (props: { message: string }) => { - return () => h('div', props.message) - }, - { props: ['message'] }, - ) - - expect(typeof RegularComp).toBe('object') - - const root = nodeOps.createElement('div') - render(h(RegularComp, { message: 'hello' }), root) - expect(serializeInner(root)).toBe(`
hello
`) - }) - - test('should work with union types in generics', () => { - const UnionGenericComp = defineComponent( - (props: { size: T }) => { - return () => h('div', props.size) - }, - { props: ['size'] }, - ) - - expect(typeof UnionGenericComp).toBe('object') - - const root1 = nodeOps.createElement('div') - render(h(UnionGenericComp, { size: 'small' }), root1) - expect(serializeInner(root1)).toBe(`
small
`) - - const root2 = nodeOps.createElement('div') - render(h(UnionGenericComp, { size: 'large' }), root2) - expect(serializeInner(root2)).toBe(`
large
`) - }) - - test('should work with array generics', () => { - const ArrayGenericComp = defineComponent( - (props: { items: T[]; selectedItem: T }) => { - return () => h('div', `${props.items.length}-${props.selectedItem}`) - }, - { props: ['items', 'selectedItem'] }, - ) - - expect(typeof ArrayGenericComp).toBe('object') - - const root1 = nodeOps.createElement('div') - render( - h(ArrayGenericComp, { - items: ['a', 'b', 'c'], - selectedItem: 'a', - }), - root1, - ) - expect(serializeInner(root1)).toBe(`
3-a
`) - - const root2 = nodeOps.createElement('div') - render( - h(ArrayGenericComp, { - items: [1, 2, 3], - selectedItem: 1, - }), - root2, - ) - expect(serializeInner(root2)).toBe(`
3-1
`) - }) -})