From 3fc1bb1a2e77dc57bf897e38ae24bc223a142ff8 Mon Sep 17 00:00:00 2001 From: zhiyuanzmj <260480378@qq.com> Date: Sat, 30 Aug 2025 21:42:41 +0800 Subject: [PATCH 01/17] feat(vapor): implement defineVaporComponent types --- .../dts-test/defineComponent.test-d.tsx | 28 +- .../dts-test/defineVaporComponent.test-d.tsx | 1250 +++++++++++++++++ .../dts-test/functionalComponent.test-d.tsx | 4 + packages-private/dts-test/tsx.test-d.tsx | 11 +- .../runtime-core/src/apiDefineComponent.ts | 2 +- packages/runtime-core/src/component.ts | 4 +- .../src/componentPublicInstance.ts | 4 +- .../runtime-core/src/components/KeepAlive.ts | 2 +- .../runtime-core/src/components/Suspense.ts | 2 +- .../runtime-core/src/components/Teleport.ts | 2 +- packages/runtime-core/src/h.ts | 2 +- packages/runtime-core/src/index.ts | 2 + .../src/components/TransitionGroup.ts | 2 +- .../__tests__/apiSetupContext.spec.ts | 4 +- .../runtime-vapor/src/apiDefineComponent.ts | 206 ++- packages/runtime-vapor/src/component.ts | 145 +- packages/runtime-vapor/src/componentSlots.ts | 2 +- packages/vue/jsx-runtime/index.d.ts | 8 +- packages/vue/jsx.d.ts | 9 +- 19 files changed, 1592 insertions(+), 97 deletions(-) create mode 100644 packages-private/dts-test/defineVaporComponent.test-d.tsx diff --git a/packages-private/dts-test/defineComponent.test-d.tsx b/packages-private/dts-test/defineComponent.test-d.tsx index 1967668dceb..b7ae8b6e63e 100644 --- a/packages-private/dts-test/defineComponent.test-d.tsx +++ b/packages-private/dts-test/defineComponent.test-d.tsx @@ -1880,20 +1880,22 @@ interface ErrorMessageSlotProps { * component types generated by vue-tsc * relying on legacy CreateComponentPublicInstance signature */ +type Props = Readonly< + vue.ExtractPropTypes<{ + as: { + type: StringConstructor + default: any + } + name: { + type: StringConstructor + required: true + } + }> & + vue.AllowedComponentProps +> declare const ErrorMessage: { - new (...args: any[]): vue.CreateComponentPublicInstance< - Readonly< - vue.ExtractPropTypes<{ - as: { - type: StringConstructor - default: any - } - name: { - type: StringConstructor - required: true - } - }> - >, + new (props: Props): vue.CreateComponentPublicInstance< + Props, () => | VNode< vue.RendererNode, diff --git a/packages-private/dts-test/defineVaporComponent.test-d.tsx b/packages-private/dts-test/defineVaporComponent.test-d.tsx new file mode 100644 index 00000000000..47b4ef459ea --- /dev/null +++ b/packages-private/dts-test/defineVaporComponent.test-d.tsx @@ -0,0 +1,1250 @@ +import { + type AllowedComponentProps, + type Block, + type Component, + type ComponentCustomProps, + type DefineVaporComponent, + type EmitsOptions, + type ExtractPropTypes, + type GenericComponentInstance, + type PropType, + type VaporComponentInstance, + type VaporPublicProps, + createApp, + createComponent, + createVaporApp, + defineVaporComponent, + reactive, + ref, +} from 'vue' +import { type IsAny, type IsUnion, describe, expectType } from './utils' + +describe('with object props', () => { + interface ExpectedProps { + a?: number | undefined + aa: number + aaa: number | null + aaaa: number | undefined + b: string + e?: Function + h: boolean + j: undefined | (() => string | undefined) + bb: string + bbb: string + bbbb: string | undefined + bbbbb: string | undefined + cc?: string[] | undefined + dd: { n: 1 } + ee?: () => string + ff?: (a: number, b: string) => { a: boolean } + ccc?: string[] | undefined + ddd: string[] + eee: () => { a: string } + fff: (a: number, b: string) => { a: boolean } + hhh: boolean + ggg: 'foo' | 'bar' + ffff: (a: number, b: string) => { a: boolean } + iii?: (() => string) | (() => number) + jjj: ((arg1: string) => string) | ((arg1: string, arg2: string) => string) + kkk?: any + validated?: string + date?: Date + l?: Date + ll?: Date | number + lll?: string | number + } + + type GT = string & { __brand: unknown } + + const props = { + a: Number, + aa: { + type: Number as PropType, + default: 1, + }, + aaa: { + type: Number as PropType, + default: 1, + }, + aaaa: { + type: Number as PropType, + // `as const` prevents widening to `boolean` (keeps literal `true` type) + required: true as const, + }, + // required should make property non-void + b: { + type: String, + required: true as true, + }, + e: Function, + h: Boolean, + j: Function as PropType string | undefined)>, + // default value should infer type and make it non-void + bb: { + default: 'hello', + }, + bbb: { + // Note: default function value requires arrow syntax + explicit + // annotation + default: (props: any) => (props.bb as string) || 'foo', + }, + bbbb: { + type: String, + default: undefined, + }, + bbbbb: { + type: String, + default: () => undefined, + }, + // explicit type casting + cc: Array as PropType, + // required + type casting + dd: { + type: Object as PropType<{ n: 1 }>, + required: true as true, + }, + // return type + ee: Function as PropType<() => string>, + // arguments + object return + ff: Function as PropType<(a: number, b: string) => { a: boolean }>, + // explicit type casting with constructor + ccc: Array as () => string[], + // required + constructor type casting + ddd: { + type: Array as () => string[], + required: true as true, + }, + // required + object return + eee: { + type: Function as PropType<() => { a: string }>, + required: true as true, + }, + // required + arguments + object return + fff: { + type: Function as PropType<(a: number, b: string) => { a: boolean }>, + required: true as true, + }, + hhh: { + type: Boolean, + required: true as true, + }, + // default + type casting + ggg: { + type: String as PropType<'foo' | 'bar'>, + default: 'foo', + }, + // default + function + ffff: { + type: Function as PropType<(a: number, b: string) => { a: boolean }>, + default: (a: number, b: string) => ({ a: a > +b }), + }, + // union + function with different return types + iii: Function as PropType<(() => string) | (() => number)>, + // union + function with different args & same return type + jjj: { + type: Function as PropType< + ((arg1: string) => string) | ((arg1: string, arg2: string) => string) + >, + required: true as true, + }, + kkk: null, + validated: { + type: String, + // validator requires explicit annotation + validator: (val: unknown) => val !== '', + }, + date: Date, + l: [Date], + ll: [Date, Number], + lll: [String, Number], + } + + const MyComponent = defineVaporComponent({ + props, + setup(props) { + // type assertion. See https://github.com/SamVerschueren/tsd + expectType(props.a) + expectType(props.aa) + expectType(props.aaa) + + // @ts-expect-error should included `undefined` + expectType(props.aaaa) + expectType(props.aaaa) + + expectType(props.b) + expectType(props.e) + expectType(props.h) + expectType(props.j) + expectType(props.bb) + expectType(props.bbb) + expectType(props.bbbb) + expectType(props.bbbbb) + expectType(props.cc) + expectType(props.dd) + expectType(props.ee) + expectType(props.ff) + expectType(props.ccc) + expectType(props.ddd) + expectType(props.eee) + expectType(props.fff) + expectType(props.hhh) + expectType(props.ggg) + expectType(props.ffff) + if (typeof props.iii !== 'function') { + expectType(props.iii) + } + expectType(props.iii) + expectType>(true) + expectType(props.jjj) + expectType(props.kkk) + expectType(props.validated) + expectType(props.date) + expectType(props.l) + expectType(props.ll) + expectType(props.lll) + + // @ts-expect-error props should be readonly + props.a = 1 + + // setup context + return { + c: ref(1), + d: { + e: ref('hi'), + }, + f: reactive({ + g: ref('hello' as GT), + }), + } + }, + render(ctx, props) { + expectType(props.a) + expectType(props.aa) + expectType(props.aaa) + expectType(props.b) + expectType(props.e) + expectType(props.h) + expectType(props.bb) + expectType(props.cc) + expectType(props.dd) + expectType(props.ee) + expectType(props.ff) + expectType(props.ccc) + expectType(props.ddd) + expectType(props.eee) + expectType(props.fff) + expectType(props.hhh) + expectType(props.ggg) + if (typeof props.iii !== 'function') { + expectType(props.iii) + } + expectType(props.iii) + expectType>(true) + expectType(props.jjj) + expectType(props.kkk) + + // @ts-expect-error props should be readonly + props.a = 1 + return [] + }, + }) + + expectType(MyComponent) + + // Test TSX + expectType( + {}} + cc={['cc']} + dd={{ n: 1 }} + ee={() => 'ee'} + ccc={['ccc']} + ddd={['ddd']} + eee={() => ({ a: 'eee' })} + fff={(a, b) => ({ a: a > +b })} + hhh={false} + ggg="foo" + jjj={() => ''} + // should allow class/style as attrs + class="bar" + style={{ color: 'red' }} + // should allow key + key={'foo'} + // should allow ref + ref={'foo'} + ref_for={true} + />, + ) + + expectType( + ({ a: 'eee' })} + fff={(a, b) => ({ a: a > +b })} + hhh={false} + jjj={() => ''} + />, + ) + + // @ts-expect-error missing required props + let c = + // @ts-expect-error wrong prop types + c = + // @ts-expect-error wrong prop types + c = + + // @ts-expect-error + ; + + // `this` should be void inside of prop validators and prop default factories + defineVaporComponent({ + props: { + myProp: { + type: Number, + validator(val: unknown): boolean { + // @ts-expect-error + return val !== this.otherProp + }, + default(): number { + // @ts-expect-error + return this.otherProp + 1 + }, + }, + otherProp: { + type: Number, + required: true, + }, + }, + }) +}) + +describe('type inference w/ optional props declaration', () => { + const MyComponent = defineVaporComponent<{ a: string[]; msg: string }>({ + setup(props) { + expectType(props.msg) + expectType(props.a) + return { + b: 1, + } + }, + }) + + expectType() + // @ts-expect-error + ; + // @ts-expect-error + ; +}) + +describe('type inference w/ direct setup function', () => { + const MyComponent = defineVaporComponent((_props: { msg: string }) => []) + expectType() + // @ts-expect-error + ; + // @ts-expect-error + ; +}) + +describe('type inference w/ array props declaration', () => { + const MyComponent = defineVaporComponent({ + props: ['a', 'b'], + setup(props) { + // @ts-expect-error props should be readonly + props.a = 1 + expectType(props.a) + expectType(props.b) + return { + c: 1, + } + }, + render(ctx, props) { + expectType(props.a) + expectType(props.b) + // @ts-expect-error + props.a = 1 + expectType(ctx.c) + return [] + }, + }) + expectType() + // @ts-expect-error + ; +}) + +// #4051 +describe('type inference w/ empty prop object', () => { + const MyComponent = defineVaporComponent({ + props: {}, + setup(props) { + return {} + }, + render() { + return [] + }, + }) + expectType() + // AllowedComponentProps + expectType() + // ComponentCustomProps + expectType() + // VNodeProps + expectType() + // @ts-expect-error + expectError() +}) + +describe('compatibility w/ createComponent', () => { + const comp = defineVaporComponent({}) + createComponent(comp) + + const comp2 = defineVaporComponent({ + props: { foo: String }, + }) + createComponent(comp2) + + const comp3 = defineVaporComponent({ + setup() { + return { + a: 1, + } + }, + }) + createComponent(comp3) + + const comp4 = defineVaporComponent(() => []) + createComponent(comp4) +}) + +describe('compatibility w/ createApp', () => { + const comp = defineVaporComponent({}) + createApp(comp).mount('#hello') + + const comp2 = defineVaporComponent({ + props: { foo: String }, + }) + createVaporApp(comp2).mount('#hello') + + const comp3 = defineVaporComponent({ + setup() { + return { + a: 1, + } + }, + }) + createVaporApp(comp3).mount('#hello') +}) + +describe('emits', () => { + // Note: for TSX inference, ideally we want to map emits to onXXX props, + // but that requires type-level string constant concatenation as suggested in + // https://github.com/Microsoft/TypeScript/issues/12754 + + // The workaround for TSX users is instead of using emits, declare onXXX props + // and call them instead. Since `v-on:click` compiles to an `onClick` prop, + // this would also support other users consuming the component in templates + // with `v-on` listeners. + + // with object emits + defineVaporComponent({ + emits: { + click: (n: number) => typeof n === 'number', + input: (b: string) => b.length > 1, + Focus: (f: boolean) => !!f, + }, + setup(props, { emit }) { + emit('click', 1) + emit('input', 'foo') + emit('Focus', true) + // @ts-expect-error + emit('nope') + // @ts-expect-error + emit('click') + // @ts-expect-error + emit('click', 'foo') + // @ts-expect-error + emit('input') + // @ts-expect-error + emit('input', 1) + // @ts-expect-error + emit('focus') + // @ts-expect-error + emit('focus', true) + }, + }) + + // with array emits + defineVaporComponent({ + emits: ['foo', 'bar'], + setup(props, { emit }) { + emit('foo') + emit('foo', 123) + emit('bar') + // @ts-expect-error + emit('nope') + }, + }) + + // with tsx + const Component = defineVaporComponent({ + emits: { + click: (n: number) => typeof n === 'number', + }, + setup(props, { emit }) { + emit('click', 1) + // @ts-expect-error + emit('click') + // @ts-expect-error + emit('click', 'foo') + }, + }) + + defineVaporComponent({ + render() { + return ( + { + return n + 1 + }} + /> + ) + }, + }) + + // #11803 manual props annotation in setup() + const Hello = defineVaporComponent({ + name: 'HelloWorld', + inheritAttrs: false, + props: { foo: String }, + emits: { + customClick: (args: string) => typeof args === 'string', + }, + setup(props: { foo?: string }) {}, + }) + ; {}} /> + + // without emits + defineVaporComponent({ + setup(props, { emit }) { + emit('test', 1) + emit('test') + }, + }) + + // emit should be valid when GenericComponentInstance is used. + const instance = {} as GenericComponentInstance + instance.emit('test', 1) + instance.emit('test') + + // `this` should be void inside of emits validators + defineVaporComponent({ + props: ['bar'], + emits: { + foo(): boolean { + // @ts-expect-error + return this.bar === 3 + }, + }, + }) +}) + +describe('extract instance type', () => { + const CompA = defineVaporComponent({ + props: { + a: { + type: Boolean, + default: false, + }, + b: { + type: String, + required: true, + }, + c: Number, + }, + }) + + const compA = {} as InstanceType + + expectType(compA.props.a) + expectType(compA.props.b) + expectType(compA.props.c) + + // @ts-expect-error + compA.props.a = true + // @ts-expect-error + compA.props.b = 'foo' + // @ts-expect-error + compA.props.c = 1 +}) + +describe('async setup', () => { + type GT = string & { __brand: unknown } + const Comp = defineVaporComponent({ + async setup() { + // setup context + return { + a: ref(1), + b: { + c: ref('hi'), + }, + d: reactive({ + e: ref('hello' as GT), + }), + } + }, + render(ctx) { + // assert setup context unwrapping + expectType(ctx.a) + expectType(ctx.b.c.value) + expectType(ctx.d.e) + + // setup context properties should be mutable + ctx.a = 2 + return [] + }, + }) + + const vm = {} as InstanceType + if (vm.exposeProxy) { + // assert setup context unwrapping + expectType(vm.exposeProxy.a) + expectType(vm.exposeProxy.b.c.value) + expectType(vm.exposeProxy.d.e) + + // setup context properties should be mutable + vm.exposeProxy.a = 2 + } +}) + +// #5948 +describe('defineVaporComponent should infer correct types when assigning to Component', () => { + let component: Component + component = defineVaporComponent({ + setup(_, { attrs, slots }) { + // @ts-expect-error should not be any + expectType<[]>(attrs) + // @ts-expect-error should not be any + expectType<[]>(slots) + }, + }) + expectType(component) +}) + +// #5969 +describe('should allow to assign props', () => { + const Child = defineVaporComponent({ + props: { + bar: String, + }, + }) + + const Parent = defineVaporComponent({ + props: { + ...Child.props, + foo: String, + }, + }) + + const child = new Child() + expectType() +}) + +// #6052 +describe('prop starting with `on*` is broken', () => { + defineVaporComponent({ + props: { + onX: { + type: Function as PropType<(a: 1) => void>, + required: true, + }, + }, + setup(props) { + expectType<(a: 1) => void>(props.onX) + props.onX(1) + }, + }) + + defineVaporComponent({ + props: { + onX: { + type: Function as PropType<(a: 1) => void>, + required: true, + }, + }, + emits: { + test: (a: 1) => true, + }, + setup(props) { + expectType<(a: 1) => void>(props.onX) + }, + }) +}) + +describe('function syntax w/ generics', () => { + const Comp = defineVaporComponent( + // TODO: babel plugin to auto infer runtime props options from type + // similar to defineProps<{...}>() + (props: { msg: T; list: T[] }) => { + // use Composition API here like in