From 1b8408d87e891ce98146a311e012bc38589011b8 Mon Sep 17 00:00:00 2001 From: Hanse Kim Date: Fri, 2 Aug 2024 15:05:46 +0900 Subject: [PATCH 1/7] feat(tsx): add support for passing generics to child components --- .../runtime-core/src/apiDefineComponent.ts | 119 ++++++++++++++++-- packages/runtime-core/src/index.ts | 1 + packages/runtime-dom/src/index.ts | 2 +- 3 files changed, 111 insertions(+), 11 deletions(-) diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 98e9ae7952c..c0279eb67ff 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -28,7 +28,7 @@ import type { TypeEmitsToOptions, } from './componentEmits' import { extend, isFunction } from '@vue/shared' -import type { VNodeProps } from './vnode' +import type { VNode, VNodeProps } from './vnode' import type { ComponentPublicInstanceConstructor, CreateComponentPublicInstanceWithMixins, @@ -36,6 +36,7 @@ import type { import type { SlotsType } from './componentSlots' import type { Directive } from './directives' import type { ComponentTypeEmits } from './apiSetupHelpers' +import type { Ref } from '@vue/reactivity' export type PublicProps = VNodeProps & AllowedComponentProps & @@ -132,6 +133,60 @@ export type DefineSetupFnComponent< S > +export type DefineComponentWithGeneric< + PropsOrPropOptions = {}, + RawBindings = {}, + D = {}, + C extends ComputedOptions = ComputedOptions, + M extends MethodOptions = MethodOptions, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, + E extends EmitsOptions = {}, + EE extends string = string, + PP = PublicProps, + Props = ResolveProps, + Defaults = ExtractDefaultPropTypes, + S extends SlotsType = {}, + Generic extends Record = {}, +> = ComponentPublicInstanceConstructor< + CreateComponentPublicInstanceWithMixins< + Props, + RawBindings, + D, + C, + M, + Mixin, + Extends, + E, + PP & Props, + Defaults, + true, + {}, + S + > & + Generic +> & + ComponentOptionsBase< + Props, + RawBindings, + D, + C, + M, + Mixin, + Extends, + E, + EE, + Defaults, + {}, + string, + S + > & + PP & { + = Generic>( + props: Props & { ref?: Ref }, + ): VNode + } + // defineComponent is a utility that is primarily used for type inference // when declaring components. Type inference is provided in the component // options (provided as the argument). The returned value has artificial types @@ -282,16 +337,60 @@ export function defineComponent< unknown extends TypeProps ? true : false > +export function defineComponent< + T extends Record = {}, + PropsOptions extends Readonly = {}, + RawBindings = {}, + D = {}, + C extends ComputedOptions = {}, + M extends MethodOptions = {}, + Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, + Extends extends ComponentOptionsMixin = ComponentOptionsMixin, + E extends EmitsOptions = {}, + EE extends string = string, + S extends SlotsType = {}, +>( + options: ComponentOptionsBase< + PropsOptions, + RawBindings, + D, + C, + M, + Mixin, + Extends, + E, + EE, + ExtractDefaultPropTypes, + {}, + string, + S + > & { __generic?: T }, +): DefineComponentWithGeneric< + PropsOptions, + RawBindings, + D, + C, + M, + Mixin, + Extends, + E, + EE, + PublicProps, + Readonly, + ExtractDefaultPropTypes, + S, + T +> + // implementation, close to no-op /*! #__NO_SIDE_EFFECTS__ */ -export function defineComponent( - options: unknown, - extraOptions?: ComponentOptions, -) { - return isFunction(options) - ? // #8326: extend call and options.name access are considered side-effects - // by Rollup, so we have to wrap it in a pure-annotated IIFE. - /*#__PURE__*/ (() => - extend({ name: options.name }, extraOptions, { setup: options }))() +export function defineComponent(options: unknown): any { + const comp = isFunction(options) + ? { setup: options, name: options.name } : options + return new Proxy(comp as any, { + apply(target, thisArg, argumentsList) { + return extend({}, target, { props: argumentsList[0] }) + }, + }) } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index b8dc513689e..ab6e2c068eb 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -265,6 +265,7 @@ export type { export type { DefineComponent, DefineSetupFnComponent, + DefineComponentWithGeneric, PublicProps, } from './apiDefineComponent' export type { diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index ab85720faa8..fb2f9c3b844 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -1,7 +1,7 @@ import { type App, type CreateAppFunction, - type DefineComponent, + type DefineComponentWithGeneric as DefineComponent, DeprecationTypes, type Directive, type ElementNamespace, From 0f03fcf6fdb86982a10f6e71f37759a8c51669f5 Mon Sep 17 00:00:00 2001 From: Hanse Kim Date: Fri, 2 Aug 2024 15:06:40 +0900 Subject: [PATCH 2/7] test: add a component test --- .../runtime-core/__tests__/apiOptions.spec.ts | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/packages/runtime-core/__tests__/apiOptions.spec.ts b/packages/runtime-core/__tests__/apiOptions.spec.ts index 1d4e805efce..e2abbf64154 100644 --- a/packages/runtime-core/__tests__/apiOptions.spec.ts +++ b/packages/runtime-core/__tests__/apiOptions.spec.ts @@ -3,6 +3,7 @@ */ import type { Mock } from 'vitest' import { + type PropType, type TestElement, computed, createApp, @@ -17,6 +18,7 @@ import { triggerEvent, } from '@vue/runtime-test' import { render as domRender } from 'vue' +import type { Ref } from '@vue/reactivity' describe('api: options', () => { test('data', async () => { @@ -1704,5 +1706,104 @@ describe('api: options', () => { `Computed property "foo" is already defined in Data.`, ).toHaveBeenWarned() }) + + test('defineComponent with generic', async () => { + const Comp = defineComponent({ + props: { + msg: String, + list: Array as PropType<{ name: string }[]>, + }, + setup(props) { + const count = ref(0) + const increment = () => { + count.value++ + } + + return () => + h('div', { onClick: increment }, [ + h('h2', props.msg), + h('p', count.value), + ...(props.list || []).map((item: { name: string }) => + h('p', item.name), + ), + ]) + }, + }) + + const list: Ref<{ name: string }[]> = ref([ + { name: 'Tom' }, + { name: 'Jerry' }, + ]) + + const root = nodeOps.createElement('div') + render(h(Comp, { msg: 'Hello', list: list.value }), root) + + expect(serializeInner(root)).toBe( + `

Hello

0

Tom

Jerry

`, + ) + + triggerEvent(root.children[0] as TestElement, 'click') + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

`, + ) + + list.value.push({ name: 'Spike' }) + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

Spike

`, + ) + }) + + test('defineComponent with generic in render function', async () => { + const Comp = defineComponent({ + props: { + msg: String, + list: Array as PropType<{ name: string }[]>, + }, + setup(props) { + const count = ref(0) + const increment = () => { + count.value++ + } + + return () => + h('div', { onClick: increment }, [ + h('h2', props.msg), + h('p', count.value), + ...(props.list || []).map((item: { name: string }) => + h('p', item.name), + ), + ]) + }, + }) + + const list = ref([{ name: 'Tom' }, { name: 'Jerry' }]) + + const App = defineComponent({ + setup() { + return () => h(Comp, { msg: 'Hello', list: list.value }) + }, + }) + + const root = nodeOps.createElement('div') + render(h(App), root) + + expect(serializeInner(root)).toBe( + `

Hello

0

Tom

Jerry

`, + ) + + triggerEvent(root.children[0] as TestElement, 'click') + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

`, + ) + + list.value.push({ name: 'Spike' }) + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

Spike

`, + ) + }) }) }) From 77d5e88608d5c66f8bd5d87058e440b7115ec2bf Mon Sep 17 00:00:00 2001 From: Andy Li Date: Thu, 15 Aug 2024 10:28:40 +0800 Subject: [PATCH 3/7] fix(types/custom-element): `defineCustomElement` with required props (#11578) --- .../dts-test/defineCustomElement.test-d.ts | 33 +++++++++++++++++++ packages/runtime-dom/src/apiCustomElement.ts | 8 ++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/dts-test/defineCustomElement.test-d.ts b/packages/dts-test/defineCustomElement.test-d.ts index b81c0befe9f..f81f8b8fa61 100644 --- a/packages/dts-test/defineCustomElement.test-d.ts +++ b/packages/dts-test/defineCustomElement.test-d.ts @@ -99,4 +99,37 @@ describe('defineCustomElement using defineComponent return type', () => { expectType(instance.a) instance.a = 42 }) + + test('with required props', () => { + const Comp1Vue = defineComponent({ + props: { + a: { type: Number, required: true }, + }, + }) + const Comp = defineCustomElement(Comp1Vue) + expectType(Comp) + + const instance = new Comp() + expectType(instance.a) + instance.a = 42 + }) + + test('with default props', () => { + const Comp1Vue = defineComponent({ + props: { + a: { + type: Number, + default: 1, + validator: () => true, + }, + }, + emits: ['click'], + }) + const Comp = defineCustomElement(Comp1Vue) + expectType(Comp) + + const instance = new Comp() + expectType(instance.a) + instance.a = 42 + }) }) diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index efee4d8a9c1..2b472e73027 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -9,6 +9,7 @@ import { type ComponentOptionsBase, type ComponentOptionsMixin, type ComponentProvideOptions, + type ComponentPublicInstance, type ComputedOptions, type ConcreteComponent, type CreateAppFunction, @@ -153,14 +154,13 @@ export function defineCustomElement< // overload 3: defining a custom element from the returned value of // `defineComponent` export function defineCustomElement< - T extends DefineComponent, + // this should be `ComponentPublicInstanceConstructor` but that type is not exported + T extends { new (...args: any[]): ComponentPublicInstance }, >( options: T, extraOptions?: CustomElementOptions, ): VueElementConstructor< - T extends DefineComponent - ? ExtractPropTypes

- : unknown + T extends DefineComponent ? P : unknown > /*! #__NO_SIDE_EFFECTS__ */ From 5545aa242f6b4586684e384c2acaebf8fd519ae3 Mon Sep 17 00:00:00 2001 From: Hanse Kim Date: Fri, 16 Aug 2024 21:35:35 +0900 Subject: [PATCH 4/7] fix(test): move test to defineComponent.test-d.tsx --- .../dts-test/defineComponent.test-d.tsx | 126 ++++++++++++++++++ .../runtime-core/__tests__/apiOptions.spec.ts | 101 -------------- 2 files changed, 126 insertions(+), 101 deletions(-) diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index 79ce6d6956d..66d323b68dd 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -3,6 +3,7 @@ import { type ComponentOptions, type ComponentPublicInstance, type PropType, + type Ref, type SetupContext, type Slots, type SlotsType, @@ -10,12 +11,21 @@ import { createApp, defineComponent, h, + nextTick, reactive, ref, + render as vueRender, withKeys, withModifiers, } from 'vue' import { type IsAny, type IsUnion, describe, expectType } from './utils' +import { + type TestElement, + type TestNode, + nodeOps, + serializeInner, + triggerEvent, +} from '@vue/runtime-test' describe('with object props', () => { interface ExpectedProps { @@ -2027,3 +2037,119 @@ expectString(instance.actionText) // public prop on $props should be optional // @ts-expect-error expectString(instance.$props.actionText) + +// Helper function to safely cast TestNode to TestElement +function getFirstElementChild(node: TestNode): TestElement { + if ('children' in node) { + return node.children[0] as TestElement + } + throw new Error('Expected an element with children') +} + +// Custom render function with correct typing for TestElement +function render(vnode: any, root: TestElement) { + return vueRender(vnode, root as any) +} + +describe('defineComponent with generics', () => { + test('defineComponent with generic', async () => { + const Comp = defineComponent({ + props: { + msg: String, + list: Array as PropType<{ name: string }[]>, + }, + setup(props) { + const count = ref(0) + const increment = () => { + count.value++ + } + + return () => + h('div', { onClick: increment }, [ + h('h2', props.msg), + h('p', count.value), + ...(props.list || []).map((item: { name: string }) => + h('p', item.name), + ), + ]) + }, + }) + + const list: Ref<{ name: string }[]> = ref([ + { name: 'Tom' }, + { name: 'Jerry' }, + ]) + + const root = nodeOps.createElement('div') + render(h(Comp, { msg: 'Hello', list: list.value }), root) + + expect(serializeInner(root)).toBe( + `

Hello

0

Tom

Jerry

`, + ) + + const firstChild = getFirstElementChild(root) + triggerEvent(firstChild, 'click') + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

`, + ) + + list.value.push({ name: 'Spike' }) + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

Spike

`, + ) + }) + + test('defineComponent with generic in render function', async () => { + const Comp = defineComponent({ + props: { + msg: String, + list: Array as PropType<{ name: string }[]>, + }, + setup(props) { + const count = ref(0) + const increment = () => { + count.value++ + } + + return () => + h('div', { onClick: increment }, [ + h('h2', props.msg), + h('p', count.value), + ...(props.list || []).map((item: { name: string }) => + h('p', item.name), + ), + ]) + }, + }) + + const list = ref([{ name: 'Tom' }, { name: 'Jerry' }]) + + const App = defineComponent({ + setup() { + return () => h(Comp, { msg: 'Hello', list: list.value }) + }, + }) + + const root = nodeOps.createElement('div') + render(h(App), root) + + expect(serializeInner(root)).toBe( + `

Hello

0

Tom

Jerry

`, + ) + + const firstChild = getFirstElementChild(root) + triggerEvent(firstChild, 'click') + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

`, + ) + + list.value.push({ name: 'Spike' }) + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

Spike

`, + ) + }) +}) diff --git a/packages/runtime-core/__tests__/apiOptions.spec.ts b/packages/runtime-core/__tests__/apiOptions.spec.ts index e2abbf64154..1d4e805efce 100644 --- a/packages/runtime-core/__tests__/apiOptions.spec.ts +++ b/packages/runtime-core/__tests__/apiOptions.spec.ts @@ -3,7 +3,6 @@ */ import type { Mock } from 'vitest' import { - type PropType, type TestElement, computed, createApp, @@ -18,7 +17,6 @@ import { triggerEvent, } from '@vue/runtime-test' import { render as domRender } from 'vue' -import type { Ref } from '@vue/reactivity' describe('api: options', () => { test('data', async () => { @@ -1706,104 +1704,5 @@ describe('api: options', () => { `Computed property "foo" is already defined in Data.`, ).toHaveBeenWarned() }) - - test('defineComponent with generic', async () => { - const Comp = defineComponent({ - props: { - msg: String, - list: Array as PropType<{ name: string }[]>, - }, - setup(props) { - const count = ref(0) - const increment = () => { - count.value++ - } - - return () => - h('div', { onClick: increment }, [ - h('h2', props.msg), - h('p', count.value), - ...(props.list || []).map((item: { name: string }) => - h('p', item.name), - ), - ]) - }, - }) - - const list: Ref<{ name: string }[]> = ref([ - { name: 'Tom' }, - { name: 'Jerry' }, - ]) - - const root = nodeOps.createElement('div') - render(h(Comp, { msg: 'Hello', list: list.value }), root) - - expect(serializeInner(root)).toBe( - `

Hello

0

Tom

Jerry

`, - ) - - triggerEvent(root.children[0] as TestElement, 'click') - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

`, - ) - - list.value.push({ name: 'Spike' }) - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

Spike

`, - ) - }) - - test('defineComponent with generic in render function', async () => { - const Comp = defineComponent({ - props: { - msg: String, - list: Array as PropType<{ name: string }[]>, - }, - setup(props) { - const count = ref(0) - const increment = () => { - count.value++ - } - - return () => - h('div', { onClick: increment }, [ - h('h2', props.msg), - h('p', count.value), - ...(props.list || []).map((item: { name: string }) => - h('p', item.name), - ), - ]) - }, - }) - - const list = ref([{ name: 'Tom' }, { name: 'Jerry' }]) - - const App = defineComponent({ - setup() { - return () => h(Comp, { msg: 'Hello', list: list.value }) - }, - }) - - const root = nodeOps.createElement('div') - render(h(App), root) - - expect(serializeInner(root)).toBe( - `

Hello

0

Tom

Jerry

`, - ) - - triggerEvent(root.children[0] as TestElement, 'click') - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

`, - ) - - list.value.push({ name: 'Spike' }) - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

Spike

`, - ) - }) }) }) From ee510d078d1294cc1f80ed5f2527bb9766ef9888 Mon Sep 17 00:00:00 2001 From: Hanse Kim Date: Tue, 20 Aug 2024 13:32:18 +0900 Subject: [PATCH 5/7] fix: modify the test to check only test --- .../dts-test/defineComponent.test-d.tsx | 160 +++++------------- 1 file changed, 45 insertions(+), 115 deletions(-) diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index 66d323b68dd..ae519fa7cbb 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -3,7 +3,6 @@ import { type ComponentOptions, type ComponentPublicInstance, type PropType, - type Ref, type SetupContext, type Slots, type SlotsType, @@ -11,21 +10,12 @@ import { createApp, defineComponent, h, - nextTick, reactive, ref, - render as vueRender, withKeys, withModifiers, } from 'vue' import { type IsAny, type IsUnion, describe, expectType } from './utils' -import { - type TestElement, - type TestNode, - nodeOps, - serializeInner, - triggerEvent, -} from '@vue/runtime-test' describe('with object props', () => { interface ExpectedProps { @@ -2038,118 +2028,58 @@ expectString(instance.actionText) // @ts-expect-error expectString(instance.$props.actionText) -// Helper function to safely cast TestNode to TestElement -function getFirstElementChild(node: TestNode): TestElement { - if ('children' in node) { - return node.children[0] as TestElement - } - throw new Error('Expected an element with children') -} - -// Custom render function with correct typing for TestElement -function render(vnode: any, root: TestElement) { - return vueRender(vnode, root as any) -} +describe('__typeRefs typing', () => { + const ChildComponent = defineComponent({ + setup() { + return { + childMethod: () => 'child method', + childValue: ref(42), + } + }, + }) -describe('defineComponent with generics', () => { - test('defineComponent with generic', async () => { - const Comp = defineComponent({ - props: { - msg: String, - list: Array as PropType<{ name: string }[]>, - }, - setup(props) { - const count = ref(0) - const increment = () => { - count.value++ - } + const ParentComponent = defineComponent({ + __typeRefs: {} as { + childRef: InstanceType + }, + setup() { + const childRef = ref | null>(null) - return () => - h('div', { onClick: increment }, [ - h('h2', props.msg), - h('p', count.value), - ...(props.list || []).map((item: { name: string }) => - h('p', item.name), - ), - ]) - }, - }) + expectType(childRef.value?.childMethod()) + expectType(childRef.value?.childValue) - const list: Ref<{ name: string }[]> = ref([ - { name: 'Tom' }, - { name: 'Jerry' }, - ]) - - const root = nodeOps.createElement('div') - render(h(Comp, { msg: 'Hello', list: list.value }), root) - - expect(serializeInner(root)).toBe( - `

Hello

0

Tom

Jerry

`, - ) - - const firstChild = getFirstElementChild(root) - triggerEvent(firstChild, 'click') - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

`, - ) - - list.value.push({ name: 'Spike' }) - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

Spike

`, - ) + return { childRef } + }, }) - test('defineComponent with generic in render function', async () => { - const Comp = defineComponent({ - props: { - msg: String, - list: Array as PropType<{ name: string }[]>, - }, - setup(props) { - const count = ref(0) - const increment = () => { - count.value++ - } + const parentInstance = new ParentComponent() - return () => - h('div', { onClick: increment }, [ - h('h2', props.msg), - h('p', count.value), - ...(props.list || []).map((item: { name: string }) => - h('p', item.name), - ), - ]) - }, - }) - - const list = ref([{ name: 'Tom' }, { name: 'Jerry' }]) + // Test typing of $refs + expectType | undefined>( + parentInstance.$refs.childRef, + ) - const App = defineComponent({ - setup() { - return () => h(Comp, { msg: 'Hello', list: list.value }) - }, - }) + // Test access to child component methods and refs through $refs + expectType(parentInstance.$refs.childRef?.childMethod()) + expectType(parentInstance.$refs.childRef?.childValue) - const root = nodeOps.createElement('div') - render(h(App), root) - - expect(serializeInner(root)).toBe( - `

Hello

0

Tom

Jerry

`, - ) - - const firstChild = getFirstElementChild(root) - triggerEvent(firstChild, 'click') - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

`, - ) - - list.value.push({ name: 'Spike' }) - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

Spike

`, - ) + // Test with multiple refs + const MultiRefComponent = defineComponent({ + __typeRefs: {} as { + stringRef: string + numberRef: number + componentRef: InstanceType + }, + setup() { + return {} + }, }) + + const multiRefInstance = new MultiRefComponent() + + expectType(multiRefInstance.$refs.stringRef) + expectType(multiRefInstance.$refs.numberRef) + expectType | undefined>( + multiRefInstance.$refs.componentRef, + ) }) From afeda80da58683dd32f2d7fd537544b6c3493da2 Mon Sep 17 00:00:00 2001 From: Hanse Kim Date: Sun, 25 Aug 2024 22:15:08 +0900 Subject: [PATCH 6/7] Revert "fix: modify the test to check only test" This reverts commit ee510d078d1294cc1f80ed5f2527bb9766ef9888. --- .../dts-test/defineComponent.test-d.tsx | 160 +++++++++++++----- 1 file changed, 115 insertions(+), 45 deletions(-) diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index ae519fa7cbb..66d323b68dd 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -3,6 +3,7 @@ import { type ComponentOptions, type ComponentPublicInstance, type PropType, + type Ref, type SetupContext, type Slots, type SlotsType, @@ -10,12 +11,21 @@ import { createApp, defineComponent, h, + nextTick, reactive, ref, + render as vueRender, withKeys, withModifiers, } from 'vue' import { type IsAny, type IsUnion, describe, expectType } from './utils' +import { + type TestElement, + type TestNode, + nodeOps, + serializeInner, + triggerEvent, +} from '@vue/runtime-test' describe('with object props', () => { interface ExpectedProps { @@ -2028,58 +2038,118 @@ expectString(instance.actionText) // @ts-expect-error expectString(instance.$props.actionText) -describe('__typeRefs typing', () => { - const ChildComponent = defineComponent({ - setup() { - return { - childMethod: () => 'child method', - childValue: ref(42), - } - }, - }) +// Helper function to safely cast TestNode to TestElement +function getFirstElementChild(node: TestNode): TestElement { + if ('children' in node) { + return node.children[0] as TestElement + } + throw new Error('Expected an element with children') +} - const ParentComponent = defineComponent({ - __typeRefs: {} as { - childRef: InstanceType - }, - setup() { - const childRef = ref | null>(null) +// Custom render function with correct typing for TestElement +function render(vnode: any, root: TestElement) { + return vueRender(vnode, root as any) +} - expectType(childRef.value?.childMethod()) - expectType(childRef.value?.childValue) +describe('defineComponent with generics', () => { + test('defineComponent with generic', async () => { + const Comp = defineComponent({ + props: { + msg: String, + list: Array as PropType<{ name: string }[]>, + }, + setup(props) { + const count = ref(0) + const increment = () => { + count.value++ + } - return { childRef } - }, - }) + return () => + h('div', { onClick: increment }, [ + h('h2', props.msg), + h('p', count.value), + ...(props.list || []).map((item: { name: string }) => + h('p', item.name), + ), + ]) + }, + }) - const parentInstance = new ParentComponent() + const list: Ref<{ name: string }[]> = ref([ + { name: 'Tom' }, + { name: 'Jerry' }, + ]) + + const root = nodeOps.createElement('div') + render(h(Comp, { msg: 'Hello', list: list.value }), root) + + expect(serializeInner(root)).toBe( + `

Hello

0

Tom

Jerry

`, + ) + + const firstChild = getFirstElementChild(root) + triggerEvent(firstChild, 'click') + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

`, + ) + + list.value.push({ name: 'Spike' }) + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

Spike

`, + ) + }) - // Test typing of $refs - expectType | undefined>( - parentInstance.$refs.childRef, - ) + test('defineComponent with generic in render function', async () => { + const Comp = defineComponent({ + props: { + msg: String, + list: Array as PropType<{ name: string }[]>, + }, + setup(props) { + const count = ref(0) + const increment = () => { + count.value++ + } - // Test access to child component methods and refs through $refs - expectType(parentInstance.$refs.childRef?.childMethod()) - expectType(parentInstance.$refs.childRef?.childValue) + return () => + h('div', { onClick: increment }, [ + h('h2', props.msg), + h('p', count.value), + ...(props.list || []).map((item: { name: string }) => + h('p', item.name), + ), + ]) + }, + }) - // Test with multiple refs - const MultiRefComponent = defineComponent({ - __typeRefs: {} as { - stringRef: string - numberRef: number - componentRef: InstanceType - }, - setup() { - return {} - }, - }) + const list = ref([{ name: 'Tom' }, { name: 'Jerry' }]) - const multiRefInstance = new MultiRefComponent() + const App = defineComponent({ + setup() { + return () => h(Comp, { msg: 'Hello', list: list.value }) + }, + }) - expectType(multiRefInstance.$refs.stringRef) - expectType(multiRefInstance.$refs.numberRef) - expectType | undefined>( - multiRefInstance.$refs.componentRef, - ) + const root = nodeOps.createElement('div') + render(h(App), root) + + expect(serializeInner(root)).toBe( + `

Hello

0

Tom

Jerry

`, + ) + + const firstChild = getFirstElementChild(root) + triggerEvent(firstChild, 'click') + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

`, + ) + + list.value.push({ name: 'Spike' }) + await nextTick() + expect(serializeInner(root)).toBe( + `

Hello

1

Tom

Jerry

Spike

`, + ) + }) }) From e46dfdd771ff0be4ce77a8ef0f03568fe72c564f Mon Sep 17 00:00:00 2001 From: Hanse Kim Date: Sun, 25 Aug 2024 23:25:30 +0900 Subject: [PATCH 7/7] fix: change test for component --- .../dts-test/defineComponent.test-d.tsx | 169 ++++++------------ .../runtime-core/src/apiDefineComponent.ts | 4 +- 2 files changed, 56 insertions(+), 117 deletions(-) diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index 66d323b68dd..2cc050d5ab6 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -3,7 +3,6 @@ import { type ComponentOptions, type ComponentPublicInstance, type PropType, - type Ref, type SetupContext, type Slots, type SlotsType, @@ -11,21 +10,12 @@ import { createApp, defineComponent, h, - nextTick, reactive, ref, - render as vueRender, withKeys, withModifiers, } from 'vue' import { type IsAny, type IsUnion, describe, expectType } from './utils' -import { - type TestElement, - type TestNode, - nodeOps, - serializeInner, - triggerEvent, -} from '@vue/runtime-test' describe('with object props', () => { interface ExpectedProps { @@ -2038,118 +2028,69 @@ expectString(instance.actionText) // @ts-expect-error expectString(instance.$props.actionText) -// Helper function to safely cast TestNode to TestElement -function getFirstElementChild(node: TestNode): TestElement { - if ('children' in node) { - return node.children[0] as TestElement - } - throw new Error('Expected an element with children') -} +describe('generic components in defineComponent', () => { + const GenericComp = defineComponent( + (props: { msg: string; list: T[] }) => { + return () => ( +
+ {props.msg} + {props.list.map(item => item.name).join(', ')} +
+ ) + }, + ) -// Custom render function with correct typing for TestElement -function render(vnode: any, root: TestElement) { - return vueRender(vnode, root as any) -} + const GenericCompUser = defineComponent(() => { + const list = ref([{ name: 'Tom' }, { name: 'Jack' }]) -describe('defineComponent with generics', () => { - test('defineComponent with generic', async () => { - const Comp = defineComponent({ - props: { - msg: String, - list: Array as PropType<{ name: string }[]>, - }, - setup(props) { - const count = ref(0) - const increment = () => { - count.value++ - } + return () => { + return ( +
+ msg="hello" list={list.value} /> +
+ ) + } + }) - return () => - h('div', { onClick: increment }, [ - h('h2', props.msg), - h('p', count.value), - ...(props.list || []).map((item: { name: string }) => - h('p', item.name), - ), - ]) - }, - }) + // Test correct usage + expectType() - const list: Ref<{ name: string }[]> = ref([ - { name: 'Tom' }, - { name: 'Jerry' }, - ]) + // Test GenericComp directly with correct props + expectType( + msg="hello" list={[{ name: 'Alice' }]} />, + ) - const root = nodeOps.createElement('div') - render(h(Comp, { msg: 'Hello', list: list.value }), root) - - expect(serializeInner(root)).toBe( - `

Hello

0

Tom

Jerry

`, - ) - - const firstChild = getFirstElementChild(root) - triggerEvent(firstChild, 'click') - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

`, - ) - - list.value.push({ name: 'Spike' }) - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

Spike

`, - ) - }) + // Test with missing required prop + expectType( + // @ts-expect-error + list={[{ name: 'Bob' }]} />, + ) - test('defineComponent with generic in render function', async () => { - const Comp = defineComponent({ - props: { - msg: String, - list: Array as PropType<{ name: string }[]>, - }, - setup(props) { - const count = ref(0) - const increment = () => { - count.value++ - } + // Test with extended type + interface Person { + name: string + age: number + } - return () => - h('div', { onClick: increment }, [ - h('h2', props.msg), - h('p', count.value), - ...(props.list || []).map((item: { name: string }) => - h('p', item.name), - ), - ]) - }, - }) + const ExtendedGenericCompUser = defineComponent(() => { + const people = ref([ + { name: 'Tom', age: 25 }, + { name: 'Jack', age: 30 }, + ]) - const list = ref([{ name: 'Tom' }, { name: 'Jerry' }]) + return () => { + return ( +
+ msg="people" list={people.value} /> +
+ ) + } + }) - const App = defineComponent({ - setup() { - return () => h(Comp, { msg: 'Hello', list: list.value }) - }, - }) + expectType() - const root = nodeOps.createElement('div') - render(h(App), root) - - expect(serializeInner(root)).toBe( - `

Hello

0

Tom

Jerry

`, - ) - - const firstChild = getFirstElementChild(root) - triggerEvent(firstChild, 'click') - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

`, - ) - - list.value.push({ name: 'Spike' }) - await nextTick() - expect(serializeInner(root)).toBe( - `

Hello

1

Tom

Jerry

Spike

`, - ) - }) + // Test GenericComp directly with extended type + expectType( + msg="people" list={[{ name: 'Alice', age: 28 }]} />, + ) }) diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index f35c9e278f6..e7d979d5167 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -184,9 +184,7 @@ export type DefineComponentWithGeneric< S > & PP & { - = Generic>( - props: Props & { ref?: Ref }, - ): VNode + (props: Props & { ref?: Ref } & T): VNode } // defineComponent is a utility that is primarily used for type inference