2
0
Эх сурвалжийг харах

feat(runtime-vapor): implement defineVaporComponent types (#13831)

zhiyuanzmj 6 сар өмнө
parent
commit
9d9efd493e
31 өөрчлөгдсөн 1790 нэмэгдсэн , 127 устгасан
  1. 1 1
      package.json
  2. 15 13
      packages-private/dts-test/defineComponent.test-d.tsx
  3. 2 1
      packages-private/dts-test/tsconfig.test.json
  4. 0 3
      packages-private/dts-test/utils.d.ts
  5. 1300 0
      packages-private/dts-test/vapor/defineVaporComponent.test-d.tsx
  6. 82 0
      packages-private/dts-test/vapor/functionalComponent.test-d.tsx
  7. 11 0
      packages-private/dts-test/vapor/jsx.d.ts
  8. 11 0
      packages-private/dts-test/vapor/tsconfig.json
  9. 3 1
      packages/runtime-core/src/component.ts
  10. 1 1
      packages/runtime-core/src/componentPublicInstance.ts
  11. 1 1
      packages/runtime-core/src/components/KeepAlive.ts
  12. 1 1
      packages/runtime-core/src/components/Suspense.ts
  13. 1 1
      packages/runtime-core/src/components/Teleport.ts
  14. 1 1
      packages/runtime-core/src/h.ts
  15. 1 0
      packages/runtime-core/src/index.ts
  16. 1 1
      packages/runtime-dom/src/components/TransitionGroup.ts
  17. 2 2
      packages/runtime-vapor/__tests__/apiSetupContext.spec.ts
  18. 2 1
      packages/runtime-vapor/__tests__/component.spec.ts
  19. 1 2
      packages/runtime-vapor/__tests__/componentAttrs.spec.ts
  20. 1 1
      packages/runtime-vapor/__tests__/components/Teleport.spec.ts
  21. 1 1
      packages/runtime-vapor/__tests__/customElement.spec.ts
  22. 0 9
      packages/runtime-vapor/__tests__/hmr.spec.ts
  23. 1 1
      packages/runtime-vapor/src/apiCreateFor.ts
  24. 214 7
      packages/runtime-vapor/src/apiDefineComponent.ts
  25. 103 53
      packages/runtime-vapor/src/component.ts
  26. 2 2
      packages/runtime-vapor/src/components/KeepAlive.ts
  27. 4 2
      packages/runtime-vapor/src/components/Teleport.ts
  28. 4 5
      packages/runtime-vapor/src/components/Transition.ts
  29. 12 12
      packages/runtime-vapor/src/fragment.ts
  30. 9 2
      packages/runtime-vapor/src/index.ts
  31. 2 2
      packages/runtime-vapor/src/vdomInterop.ts

+ 1 - 1
package.json

@@ -22,7 +22,7 @@
     "test-e2e-vapor": "pnpm run prepare-e2e-vapor && vitest --project e2e-vapor",
     "prepare-e2e-vapor": "node scripts/build.js -f cjs+esm-bundler+esm-bundler-runtime && pnpm run -C packages-private/vapor-e2e-test build",
     "test-dts": "run-s build-dts test-dts-only",
-    "test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json",
+    "test-dts-only": "tsc -p packages-private/dts-built-test/tsconfig.json && tsc -p ./packages-private/dts-test/tsconfig.test.json && tsc -p ./packages-private/dts-test/vapor/tsconfig.json",
     "test-coverage": "vitest run --project unit* --coverage",
     "prebench": "node scripts/build.js -pf esm-browser reactivity",
     "prebench-compare": "node scripts/build.js -pf esm-browser reactivity",

+ 15 - 13
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,

+ 2 - 1
packages-private/dts-test/tsconfig.test.json

@@ -5,7 +5,8 @@
     "module": "esnext",
     "strict": true,
     "moduleResolution": "node",
-    "lib": ["esnext", "dom"]
+    "lib": ["esnext", "dom"],
+    "types": ["vue/jsx"]
   },
   "include": ["./*"]
 }

+ 0 - 3
packages-private/dts-test/utils.d.ts

@@ -1,9 +1,6 @@
 // This directory contains a number of d.ts assertions
 // use \@ts-expect-error where errors are expected.
 
-// register global JSX
-import 'vue/jsx'
-
 export function describe(_name: string, _fn: () => void): void
 export function test(_name: string, _fn: () => any): void
 

+ 1300 - 0
packages-private/dts-test/vapor/defineVaporComponent.test-d.tsx

@@ -0,0 +1,1300 @@
+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 './jsx'
+import { type IsAny, type IsUnion, describe, expectType } from '../utils'
+import '../built.test-d'
+
+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<number | undefined>,
+      default: 1,
+    },
+    aaa: {
+      type: Number as PropType<number | null>,
+      default: 1,
+    },
+    aaaa: {
+      type: Number as PropType<number | undefined>,
+      // `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<undefined | (() => 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<string[]>,
+    // 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<ExpectedProps['a']>(props.a)
+      expectType<ExpectedProps['aa']>(props.aa)
+      expectType<ExpectedProps['aaa']>(props.aaa)
+
+      // @ts-expect-error should included `undefined`
+      expectType<number>(props.aaaa)
+      expectType<ExpectedProps['aaaa']>(props.aaaa)
+
+      expectType<ExpectedProps['b']>(props.b)
+      expectType<ExpectedProps['e']>(props.e)
+      expectType<ExpectedProps['h']>(props.h)
+      expectType<ExpectedProps['j']>(props.j)
+      expectType<ExpectedProps['bb']>(props.bb)
+      expectType<ExpectedProps['bbb']>(props.bbb)
+      expectType<ExpectedProps['bbbb']>(props.bbbb)
+      expectType<ExpectedProps['bbbbb']>(props.bbbbb)
+      expectType<ExpectedProps['cc']>(props.cc)
+      expectType<ExpectedProps['dd']>(props.dd)
+      expectType<ExpectedProps['ee']>(props.ee)
+      expectType<ExpectedProps['ff']>(props.ff)
+      expectType<ExpectedProps['ccc']>(props.ccc)
+      expectType<ExpectedProps['ddd']>(props.ddd)
+      expectType<ExpectedProps['eee']>(props.eee)
+      expectType<ExpectedProps['fff']>(props.fff)
+      expectType<ExpectedProps['hhh']>(props.hhh)
+      expectType<ExpectedProps['ggg']>(props.ggg)
+      expectType<ExpectedProps['ffff']>(props.ffff)
+      if (typeof props.iii !== 'function') {
+        expectType<undefined>(props.iii)
+      }
+      expectType<ExpectedProps['iii']>(props.iii)
+      expectType<IsUnion<typeof props.jjj>>(true)
+      expectType<ExpectedProps['jjj']>(props.jjj)
+      expectType<ExpectedProps['kkk']>(props.kkk)
+      expectType<ExpectedProps['validated']>(props.validated)
+      expectType<ExpectedProps['date']>(props.date)
+      expectType<ExpectedProps['l']>(props.l)
+      expectType<ExpectedProps['ll']>(props.ll)
+      expectType<ExpectedProps['lll']>(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<ExpectedProps['a']>(props.a)
+      expectType<ExpectedProps['aa']>(props.aa)
+      expectType<ExpectedProps['aaa']>(props.aaa)
+      expectType<ExpectedProps['b']>(props.b)
+      expectType<ExpectedProps['e']>(props.e)
+      expectType<ExpectedProps['h']>(props.h)
+      expectType<ExpectedProps['bb']>(props.bb)
+      expectType<ExpectedProps['cc']>(props.cc)
+      expectType<ExpectedProps['dd']>(props.dd)
+      expectType<ExpectedProps['ee']>(props.ee)
+      expectType<ExpectedProps['ff']>(props.ff)
+      expectType<ExpectedProps['ccc']>(props.ccc)
+      expectType<ExpectedProps['ddd']>(props.ddd)
+      expectType<ExpectedProps['eee']>(props.eee)
+      expectType<ExpectedProps['fff']>(props.fff)
+      expectType<ExpectedProps['hhh']>(props.hhh)
+      expectType<ExpectedProps['ggg']>(props.ggg)
+      if (typeof props.iii !== 'function') {
+        expectType<undefined>(props.iii)
+      }
+      expectType<ExpectedProps['iii']>(props.iii)
+      expectType<IsUnion<typeof props.jjj>>(true)
+      expectType<ExpectedProps['jjj']>(props.jjj)
+      expectType<ExpectedProps['kkk']>(props.kkk)
+
+      // @ts-expect-error props should be readonly
+      props.a = 1
+      return []
+    },
+  })
+
+  expectType<Component>(MyComponent)
+
+  // Test TSX
+  expectType<JSX.Element>(
+    <MyComponent
+      a={1}
+      aaaa={1}
+      b="b"
+      bb="bb"
+      e={() => {}}
+      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<Component>(
+    <MyComponent
+      aaaa={1}
+      b="b"
+      dd={{ n: 1 }}
+      ddd={['ddd']}
+      eee={() => ({ a: 'eee' })}
+      fff={(a, b) => ({ a: a > +b })}
+      hhh={false}
+      jjj={() => ''}
+    />,
+  )
+
+  // @ts-expect-error missing required props
+  let c = <MyComponent />
+  // @ts-expect-error wrong prop types
+  c = <MyComponent a={'wrong type'} b="foo" dd={{ n: 1 }} ddd={['foo']} />
+  // @ts-expect-error wrong prop types
+  c = <MyComponent ggg="baz" />
+
+  // @ts-expect-error
+  ;<MyComponent b="foo" dd={{ n: 'string' }} ddd={['foo']} />
+
+  // `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<string>(props.msg)
+      expectType<string[]>(props.a)
+      return {
+        b: 1,
+      }
+    },
+  })
+
+  expectType<JSX.Element>(<MyComponent msg="1" a={['1']} />)
+  // @ts-expect-error
+  ;<MyComponent />
+  // @ts-expect-error
+  ;<MyComponent msg="1" />
+})
+
+describe('type inference w/ direct setup function', () => {
+  const MyComponent = defineVaporComponent((_props: { msg: string }) => [])
+  expectType<JSX.Element>(<MyComponent msg="foo" />)
+  // @ts-expect-error
+  ;<MyComponent />
+  // @ts-expect-error
+  ;<MyComponent msg={1} />
+})
+
+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<any>(props.a)
+      expectType<any>(props.b)
+      return {
+        c: 1,
+      }
+    },
+    render(ctx, props) {
+      expectType<any>(props.a)
+      expectType<any>(props.b)
+      // @ts-expect-error
+      props.a = 1
+      expectType<number>(ctx.c)
+      return []
+    },
+  })
+  expectType<JSX.Element>(<MyComponent a={[1, 2]} b="b" />)
+  // @ts-expect-error
+  ;<MyComponent other="other" />
+})
+
+// #4051
+describe('type inference w/ empty prop object', () => {
+  const MyComponent = defineVaporComponent({
+    props: {},
+    setup(props) {
+      return {}
+    },
+    render() {
+      return []
+    },
+  })
+  expectType<JSX.Element>(<MyComponent />)
+  // AllowedComponentProps
+  expectType<JSX.Element>(<MyComponent class={'foo'} />)
+  // ComponentCustomProps
+  expectType<JSX.Element>(<MyComponent custom={1} />)
+  // VNodeProps
+  expectType<JSX.Element>(<MyComponent key="1" />)
+  // @ts-expect-error
+  expectError(<MyComponent other="other" />)
+})
+
+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 (
+        <Component
+          onClick={(n: number) => {
+            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 }) {},
+  })
+  ;<Hello onCustomClick={() => {}} />
+
+  // 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<typeof CompA>
+
+  expectType<boolean | undefined>(compA.props.a)
+  expectType<string>(compA.props.b)
+  expectType<number | undefined>(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<number>(ctx.a)
+      expectType<string>(ctx.b.c.value)
+      expectType<GT>(ctx.d.e)
+
+      // setup context properties should be mutable
+      ctx.a = 2
+      return []
+    },
+  })
+
+  const vm = {} as InstanceType<typeof Comp>
+  if (vm.exposed && vm.exposeProxy) {
+    // assert setup context unwrapping
+    expectType<number>(vm.exposeProxy.a)
+    expectType<string>(vm.exposeProxy.b.c.value)
+    expectType<GT>(vm.exposeProxy.d.e)
+
+    // setup context properties should be mutable
+    vm.exposed.a.value = 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>(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<JSX.Element>(<Parent {...child.props} />)
+})
+
+// #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<{...}>()
+    <T extends string | number>(props: { msg: T; list: T[] }) => {
+      // use Composition API here like in <script setup>
+      const count = ref(0)
+
+      return (
+        <div>
+          {props.msg} {count.value}
+        </div>
+      )
+    },
+  )
+  expectType<JSX.Element>(<Comp msg="fse" list={['foo']} />)
+  expectType<JSX.Element>(<Comp msg={123} list={[123]} />)
+
+  expectType<JSX.Element>(
+    // @ts-expect-error missing prop
+    <Comp msg={123} />,
+  )
+
+  expectType<JSX.Element>(
+    // @ts-expect-error generics don't match
+    <Comp msg="fse" list={[123]} />,
+  )
+  expectType<JSX.Element>(
+    // @ts-expect-error generics don't match
+    <Comp msg={123} list={['123']} />,
+  )
+})
+
+describe('function syntax w/ emits', () => {
+  const Foo = defineVaporComponent(
+    (props: { msg: string }, ctx) => {
+      ctx.emit('foo')
+      // @ts-expect-error
+      ctx.emit('bar')
+      return []
+    },
+    {
+      emits: ['foo'],
+    },
+  )
+  expectType<JSX.Element>(<Foo msg="hi" onFoo={() => {}} />)
+  // @ts-expect-error
+  expectType<JSX.Element>(<Foo msg="hi" onBar={() => {}} />)
+
+  const Bar = defineVaporComponent(
+    (props: { msg: string }, ctx) => {
+      ctx.emit('foo', 'hi')
+      // @ts-expect-error
+      ctx.emit('foo')
+      // @ts-expect-error
+      ctx.emit('bar')
+      return []
+    },
+    {
+      emits: {
+        foo: (a: string) => true,
+      },
+    },
+  )
+  expectType<JSX.Element>(<Bar msg="hi" onFoo={(a: string) => true} />)
+  // @ts-expect-error
+  expectType<JSX.Element>(<Foo msg="hi" onBar={() => {}} />)
+})
+
+describe('function syntax w/ slots', () => {
+  // types
+  type DefaultSlot = (props: { msg: string }) => []
+  const Foo = defineVaporComponent(
+    (_props, { slots }: { slots: { default: DefaultSlot } }) => {
+      return slots.default({ msg: '' })
+    },
+  )
+  const foo = new Foo()
+  expectType<DefaultSlot>(foo.slots.default)
+
+  // runtime
+  const defaultSlot = (props: { msg: string }) => []
+  const Bar = defineVaporComponent(
+    (_props, { slots }) => {
+      return slots.default({ msg: '' })
+    },
+    {
+      slots: {
+        default: defaultSlot,
+      },
+    },
+  )
+  const bar = new Bar()
+  expectType<typeof defaultSlot>(bar.slots.default)
+})
+
+describe('function syntax w/ expose', () => {
+  // types
+  const Foo = defineVaporComponent(
+    (
+      props: { msg: string },
+      { expose }: { expose: (exposed: { msg: string }) => void },
+    ) => {
+      expose({
+        msg: props.msg,
+      })
+      return []
+    },
+  )
+  const foo = new Foo()
+  expectType<string>(foo.exposeProxy!.msg)
+
+  // runtime
+  const Bar = defineVaporComponent(
+    () => {
+      return []
+    },
+    {
+      setup: () => ({ msg: '' }),
+    },
+  )
+  const bar = new Bar()
+  expectType<string>(bar.exposeProxy!.msg)
+})
+
+describe('function syntax w/ runtime props', () => {
+  // with runtime props, the runtime props must match
+  // manual type declaration
+  defineVaporComponent(
+    (_props: { msg: string }) => {
+      return []
+    },
+    {
+      props: ['msg'],
+    },
+  )
+
+  defineVaporComponent(
+    <T extends string>(_props: { msg: T }) => {
+      return []
+    },
+    {
+      props: ['msg'],
+    },
+  )
+
+  defineVaporComponent(
+    <T extends string>(_props: { msg: T }) => {
+      return []
+    },
+    {
+      props: {
+        msg: String,
+      },
+    },
+  )
+
+  // @ts-expect-error string prop names don't match
+  defineVaporComponent(
+    (_props: { msg: string }) => {
+      return []
+    },
+    {
+      props: ['bar'],
+    },
+  )
+
+  defineVaporComponent(
+    (_props: { msg: string }) => {
+      return []
+    },
+    {
+      props: {
+        // @ts-expect-error prop type mismatch
+        msg: Number,
+      },
+    },
+  )
+
+  // @ts-expect-error prop keys don't match
+  defineVaporComponent(
+    (_props: { msg: string }, ctx) => {
+      return []
+    },
+    {
+      props: {
+        msg: String,
+        bar: String,
+      },
+    },
+  )
+})
+
+// check if defineVaporComponent can be exported
+export default {
+  // function components
+  a: defineVaporComponent(_ => []),
+  // no props
+  b: defineVaporComponent({}),
+  c: defineVaporComponent({
+    props: ['a'],
+  }),
+  d: defineVaporComponent({
+    props: {
+      a: Number,
+    },
+  }),
+}
+
+describe('slots', () => {
+  const comp1 = defineVaporComponent({
+    slots: {} as {
+      default: (scope: { foo: string; bar: number }) => Block
+      optional?: (scope: { data: string }) => Block
+      undefinedScope: (scope?: { data: string }) => Block
+      optionalUndefinedScope?: (scope?: { data: string }) => Block
+    },
+    setup(props, { slots }) {
+      expectType<(scope: { foo: string; bar: number }) => Block>(slots.default)
+      expectType<((scope: { data: string }) => Block) | undefined>(
+        slots.optional,
+      )
+
+      slots.default({ foo: 'foo', bar: 1 })
+
+      // @ts-expect-error it's optional
+      slots.optional({ data: 'foo' })
+      slots.optional?.({ data: 'foo' })
+
+      expectType<{
+        (): Block
+        (scope: undefined | { data: string }): Block
+      }>(slots.undefinedScope)
+
+      expectType<
+        { (): Block; (scope: undefined | { data: string }): Block } | undefined
+      >(slots.optionalUndefinedScope)
+
+      slots.default({ foo: 'foo', bar: 1 })
+      // @ts-expect-error it's optional
+      slots.optional({ data: 'foo' })
+      slots.optional?.({ data: 'foo' })
+      slots.undefinedScope()
+      slots.undefinedScope(undefined)
+      // @ts-expect-error
+      slots.undefinedScope('foo')
+
+      slots.optionalUndefinedScope?.()
+      slots.optionalUndefinedScope?.(undefined)
+      slots.optionalUndefinedScope?.({ data: 'foo' })
+      // @ts-expect-error
+      slots.optionalUndefinedScope()
+      // @ts-expect-error
+      slots.optionalUndefinedScope?.('foo')
+
+      expectType<typeof slots | undefined>(new comp1().slots)
+    },
+  })
+
+  const comp2 = defineVaporComponent({
+    setup(props, { slots }) {
+      // unknown slots
+      expectType<Record<string, ((...args: any[]) => Block) | undefined>>(slots)
+      expectType<((...args: any[]) => Block) | undefined>(slots.default)
+    },
+  })
+  expectType<Record<string, ((...args: any[]) => Block) | undefined>>(
+    new comp2().slots,
+  )
+
+  const comp3 = defineVaporComponent({
+    setup(
+      props,
+      { slots }: { slots: { default: (props: { foo: number }) => Block } },
+    ) {
+      expectType<(props: { foo: number }) => Block>(slots.default)
+    },
+  })
+  expectType<Record<string, (props: { foo: number }) => Block>>(
+    new comp3().slots,
+  )
+})
+
+describe('render', () => {
+  defineVaporComponent({
+    props: {
+      foo: Number,
+    },
+    emits: {
+      change: (e: number) => {},
+    },
+    slots: {} as { default: () => [] },
+    setup() {
+      return {
+        bar: '',
+      }
+    },
+    render(ctx, props, emit, attrs, slots) {
+      expectType<number | undefined>(props.foo)
+      expectType<string>(ctx.bar)
+      emit('change', 1)
+      return slots.default()
+    },
+  })
+})
+
+// #5885
+describe('should work when props type is incompatible with setup returned type ', () => {
+  type SizeType = 'small' | 'big'
+  const Comp = defineVaporComponent({
+    props: {
+      size: {
+        type: String as PropType<SizeType>,
+        required: true,
+      },
+    },
+    setup(props) {
+      expectType<SizeType>(props.size)
+      return {
+        size: 1,
+      }
+    },
+  })
+  type CompInstance = InstanceType<typeof Comp>
+
+  const CompA = {} as CompInstance
+  expectType<
+    VaporComponentInstance<{ size: SizeType }, {}, {}, { size: number }>
+  >(CompA)
+  expectType<number>(CompA.exposeProxy!.size)
+  expectType<SizeType>(CompA.props.size)
+})
+
+describe('expose typing', () => {
+  // types
+  const Foo = defineVaporComponent(
+    (
+      props: { some?: string },
+      { expose }: { expose: (exposed: { a: number; b: string }) => void },
+    ) => {
+      expose({ a: 1, b: '' })
+    },
+  )
+  const foo = new Foo()
+  // internal should still be exposed
+  foo.props
+
+  expectType<number>(foo.exposeProxy!.a)
+  expectType<string>(foo.exposeProxy!.b)
+
+  // runtime
+  const Bar = defineVaporComponent({
+    props: {
+      some: String,
+    },
+    setup() {
+      return { a: 1, b: '2', c: 1 }
+    },
+  })
+
+  const bar = new Bar()
+  // internal should still be exposed
+  bar.props
+
+  expectType<number>(bar.exposeProxy!.a)
+  expectType<string>(bar.exposeProxy!.b)
+})
+
+// code generated by tsc / vue-tsc, make sure this continues to work
+// so we don't accidentally change the args order of DefineComponent
+declare const MyButton: DefineVaporComponent<
+  {},
+  string,
+  EmitsOptions,
+  string,
+  {},
+  {},
+  Block,
+  {},
+  true,
+  Readonly<ExtractPropTypes<{}>>,
+  VaporPublicProps & AllowedComponentProps & ComponentCustomProps,
+  {},
+  {}
+>
+;<MyButton class="x" />
+
+describe('__typeProps backdoor for union type for conditional props', () => {
+  interface CommonProps {
+    size?: 'xl' | 'l' | 'm' | 's' | 'xs'
+  }
+
+  type ConditionalProps =
+    | {
+        color?: 'normal' | 'primary' | 'secondary'
+        appearance?: 'normal' | 'outline' | 'text'
+      }
+    | {
+        color: 'white'
+        appearance: 'outline'
+      }
+
+  type Props = CommonProps & ConditionalProps
+
+  const Comp = defineVaporComponent({
+    __typeProps: {} as Props,
+  })
+  // @ts-expect-error
+  ;<Comp color="white" />
+  // @ts-expect-error
+  ;<Comp color="white" appearance="normal" />
+  ;<Comp color="white" appearance="outline" />
+
+  const c = new Comp()
+
+  // @ts-expect-error
+  c.props = { color: 'white' }
+  // @ts-expect-error
+  c.props = { color: 'white', appearance: 'text' }
+  c.props = { color: 'white', appearance: 'outline' }
+})
+
+describe('__typeEmits backdoor, 3.3+ object syntax', () => {
+  type Emits = {
+    change: [id: number]
+    update: [value: string]
+  }
+
+  const Comp = defineVaporComponent({
+    __typeEmits: {} as Emits,
+    setup(props, { emit }) {
+      // @ts-expect-error
+      props.onChange?.('123')
+      // @ts-expect-error
+      props.onUpdate?.(123)
+
+      // @ts-expect-error
+      emit('foo')
+
+      emit('change', 123)
+      // @ts-expect-error
+      emit('change', '123')
+
+      emit('update', 'test')
+      // @ts-expect-error
+      emit('update', 123)
+    },
+  })
+
+  ;<Comp onChange={id => id.toFixed(2)} />
+  ;<Comp onUpdate={id => id.toUpperCase()} />
+  // @ts-expect-error
+  ;<Comp onChange={id => id.slice(1)} />
+  // @ts-expect-error
+  ;<Comp onUpdate={id => id.toFixed(2)} />
+
+  const c = new Comp()
+  // @ts-expect-error
+  c.emit('foo')
+
+  c.emit('change', 123)
+  // @ts-expect-error
+  c.emit('change', '123')
+
+  c.emit('update', 'test')
+  // @ts-expect-error
+  c.emit('update', 123)
+})
+
+describe('__typeEmits backdoor, call signature syntax', () => {
+  type Emits = {
+    (e: 'change', id: number): void
+    (e: 'update', value: string): void
+  }
+
+  const Comp = defineVaporComponent({
+    __typeEmits: {} as Emits,
+    setup(props, { emit }) {
+      // @ts-expect-error
+      props.onChange?.('123')
+      // @ts-expect-error
+      props.onUpdate?.(123)
+
+      // @ts-expect-error
+      emit('foo')
+
+      emit('change', 123)
+      // @ts-expect-error
+      emit('change', '123')
+
+      emit('update', 'test')
+      // @ts-expect-error
+      emit('update', 123)
+    },
+  })
+
+  ;<Comp onChange={id => id.toFixed(2)} />
+  ;<Comp onUpdate={id => id.toUpperCase()} />
+  // @ts-expect-error
+  ;<Comp onChange={id => id.slice(1)} />
+  // @ts-expect-error
+  ;<Comp onUpdate={id => id.toFixed(2)} />
+
+  const c = new Comp()
+  // @ts-expect-error
+  c.emit('foo')
+
+  c.emit('change', 123)
+  // @ts-expect-error
+  c.emit('change', '123')
+
+  c.emit('update', 'test')
+  // @ts-expect-error
+  c.emit('update', 123)
+})
+
+describe('__typeRefs backdoor, object syntax', () => {
+  type Refs = {
+    foo: number
+  }
+
+  const Parent = defineVaporComponent({
+    __typeRefs: {} as { child: InstanceType<typeof Child> },
+  })
+  const Child = defineVaporComponent({
+    __typeRefs: {} as Refs,
+  })
+  const c = new Parent()
+  const refs = c.refs
+
+  expectType<InstanceType<typeof Child>>(refs.child)
+  expectType<number>(refs.child.refs.foo)
+})
+
+describe('__typeEl backdoor', () => {
+  const Comp = defineVaporComponent({
+    __typeEl: {} as HTMLAnchorElement,
+  })
+  const c = new Comp()
+  expectType<HTMLAnchorElement>(c.block)
+
+  const Comp1 = defineVaporComponent({
+    render: () => document.createElement('a'),
+  })
+  const c1 = new Comp1()
+  expectType<HTMLAnchorElement>(c1.block)
+
+  const Comp2 = defineVaporComponent(() => document.createElement('a'))
+  const c2 = new Comp2()
+  expectType<HTMLAnchorElement>(c2.block)
+
+  const Comp3 = defineVaporComponent({
+    setup: () => document.createElement('a'),
+  })
+  const c3 = new Comp3()
+  expectType<HTMLAnchorElement>(c3.block)
+})
+
+defineVaporComponent({
+  props: {
+    foo: [String, null],
+  },
+  setup(props) {
+    expectType<IsAny<typeof props.foo>>(false)
+    expectType<string | null | undefined>(props.foo)
+  },
+})
+
+// #10843
+createApp({}).component(
+  'SomeComponent',
+  defineVaporComponent({
+    props: {
+      title: String,
+    },
+    setup(props) {
+      expectType<string | undefined>(props.title)
+      return {}
+    },
+  }),
+)
+
+const Comp = defineVaporComponent({
+  props: {
+    actionText: {
+      type: {} as PropType<string>,
+      default: 'Become a sponsor',
+    },
+  },
+  __typeProps: {} as {
+    actionText?: string
+  },
+})
+
+const instance = new Comp()
+function expectString(s: string) {}
+// public prop on props should be optional
+// @ts-expect-error
+expectString(instance.props.actionText)
+
+// #12122
+defineVaporComponent({
+  props: { foo: String },
+  render(ctx, props) {
+    expectType<{ readonly foo?: string }>(props)
+    // @ts-expect-error
+    expectType<string>(props)
+    return []
+  },
+})

+ 82 - 0
packages-private/dts-test/vapor/functionalComponent.test-d.tsx

@@ -0,0 +1,82 @@
+import type { FunctionalVaporComponent } from 'vue'
+import { expectType } from '../utils'
+import type { Block } from 'vue'
+
+// simple function signature
+const VaporComp = (props: { foo: number }) => [<div>123</div>]
+
+// TSX
+expectType<JSX.Element>(<VaporComp foo={1} />)
+expectType<JSX.Element>(<VaporComp foo={1} key="1" />)
+expectType<JSX.Element>(<VaporComp foo={1} ref="ref" />)
+// @ts-expect-error
+;<Foo />
+//  @ts-expect-error
+;<Foo foo="bar" />
+//  @ts-expect-error
+;<Foo baz="bar" />
+
+// Explicit signature with props + emits
+const Bar: FunctionalVaporComponent<
+  { foo: number },
+  { update: (value: number) => void }
+> = (props, { emit }) => {
+  expectType<number>(props.foo)
+
+  emit('update', 123)
+  //  @ts-expect-error
+  emit('nope')
+  //  @ts-expect-error
+  emit('update')
+  //  @ts-expect-error
+  emit('update', 'nope')
+  return []
+}
+
+// assigning runtime options
+Bar.props = {
+  foo: Number,
+}
+//  @ts-expect-error
+Bar.props = { foo: String }
+
+Bar.emits = {
+  update: value => value > 1,
+}
+//  @ts-expect-error
+Bar.emits = { baz: () => void 0 }
+
+// TSX
+expectType<JSX.Element>(<Bar foo={1} onUpdate={() => {}} />)
+//  @ts-expect-error
+;<Foo />
+//  @ts-expect-error
+;<Bar foo="bar" />
+//  @ts-expect-error
+;<Foo baz="bar" />
+
+const Quux: FunctionalVaporComponent<
+  {},
+  {},
+  {
+    default: (props: { foo: number }) => Block
+    optional?: (props: { foo: number }) => Block
+  }
+> = (props, { emit, slots }) => {
+  expectType<{
+    default: (scope: { foo: number }) => Block
+    optional?: (scope: { foo: number }) => Block
+  }>(slots)
+
+  slots.default({ foo: 123 })
+  // @ts-expect-error
+  slots.default({ foo: 'fesf' })
+
+  slots.optional?.({ foo: 123 })
+  // @ts-expect-error
+  slots.optional?.({ foo: 'fesf' })
+  // @ts-expect-error
+  slots.optional({ foo: 123 })
+  return []
+}
+;<Quux />

+ 11 - 0
packages-private/dts-test/vapor/jsx.d.ts

@@ -0,0 +1,11 @@
+import type { NativeElements, ReservedProps, VaporRenderResult } from 'vue'
+
+declare global {
+  namespace JSX {
+    export type Element = VaporRenderResult
+    export interface IntrinsicElements extends NativeElements {
+      [name: string]: any
+    }
+    export interface IntrinsicAttributes extends ReservedProps {}
+  }
+}

+ 11 - 0
packages-private/dts-test/vapor/tsconfig.json

@@ -0,0 +1,11 @@
+{
+  "compilerOptions": {
+    "noEmit": true,
+    "strict": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "jsx": "preserve",
+    "lib": ["esnext", "dom"]
+  },
+  "include": ["."]
+}

+ 3 - 1
packages/runtime-core/src/component.ts

@@ -114,7 +114,9 @@ export type Data = Record<string, unknown>
  * declare const instance: ComponentInstance<typeof MyComp>
  * ```
  */
-export type ComponentInstance<T> = T extends { new (): ComponentPublicInstance }
+export type ComponentInstance<T> = T extends {
+  new (...args: any[]): ComponentPublicInstance
+}
   ? InstanceType<T>
   : T extends FunctionalComponent<infer Props, infer Emits>
     ? ComponentPublicInstance<Props, {}, {}, {}, {}, ShortEmitsToObject<Emits>>

+ 1 - 1
packages/runtime-core/src/componentPublicInstance.ts

@@ -149,7 +149,7 @@ export type ComponentPublicInstanceConstructor<
   __isFragment?: never
   __isTeleport?: never
   __isSuspense?: never
-  new (...args: any[]): T
+  new (props?: T['$props']): T
 }
 
 /**

+ 1 - 1
packages/runtime-core/src/components/KeepAlive.ts

@@ -356,7 +356,7 @@ export const KeepAlive = (__COMPAT__
   ? /*@__PURE__*/ decorate(KeepAliveImpl)
   : KeepAliveImpl) as any as {
   __isKeepAlive: true
-  new (): {
+  new (props?: VNodeProps & KeepAliveProps): {
     $props: VNodeProps & KeepAliveProps
     $slots: {
       default(): VNode[]

+ 1 - 1
packages/runtime-core/src/components/Suspense.ts

@@ -129,7 +129,7 @@ export const Suspense = (__FEATURE_SUSPENSE__
   ? SuspenseImpl
   : null) as unknown as {
   __isSuspense: true
-  new (): {
+  new (props?: VNodeProps & SuspenseProps): {
     $props: VNodeProps & SuspenseProps
     $slots: {
       default(): VNode[]

+ 1 - 1
packages/runtime-core/src/components/Teleport.ts

@@ -514,7 +514,7 @@ function hydrateTeleport(
 // Force-casted public typing for h and TSX props inference
 export const Teleport = TeleportImpl as unknown as {
   __isTeleport: true
-  new (): {
+  new (props?: VNodeProps & TeleportProps): {
     $props: VNodeProps & TeleportProps
     $slots: {
       default(): VNode[]

+ 1 - 1
packages/runtime-core/src/h.ts

@@ -73,7 +73,7 @@ interface Constructor<P = any> {
   __isFragment?: never
   __isTeleport?: never
   __isSuspense?: never
-  new (...args: any[]): { $props: P }
+  new (props?: P): { $props: P }
 }
 
 type HTMLElementEventHandler = {

+ 1 - 0
packages/runtime-core/src/index.ts

@@ -305,6 +305,7 @@ export type {
   EmitsToProps,
   ShortEmitsToObject,
   EmitFn,
+  TypeEmitsToOptions,
 } from './componentEmits'
 export type {
   ComponentPublicInstance,

+ 1 - 1
packages/runtime-dom/src/components/TransitionGroup.ts

@@ -166,7 +166,7 @@ const TransitionGroupImpl: ComponentOptions = /*@__PURE__*/ decorate({
 })
 
 export const TransitionGroup = TransitionGroupImpl as unknown as {
-  new (): {
+  new (props?: TransitionGroupProps): {
     $props: TransitionGroupProps
   }
 }

+ 2 - 2
packages/runtime-vapor/__tests__/apiSetupContext.spec.ts

@@ -54,7 +54,7 @@ describe('api: setup context', () => {
         })
         const n = createTextNode()
         renderEffect(() => {
-          setText(n, props.count)
+          setText(n, String(props.count))
         })
         return n
       },
@@ -191,7 +191,7 @@ describe('api: setup context', () => {
           emit('inc', props.count + 1)
         })
         const n = createTextNode()
-        renderEffect(() => setText(n, props.count))
+        renderEffect(() => setText(n, String(props.count)))
         insert(n, n0)
         return n0
       },

+ 2 - 1
packages/runtime-vapor/__tests__/component.spec.ts

@@ -324,7 +324,7 @@ describe('component', () => {
       },
       setup(props) {
         const n0 = template(' ')() as any
-        renderEffect(() => setText(n0, props.count))
+        renderEffect(() => setText(n0, String(props.count)))
         return n0
       },
     })
@@ -396,6 +396,7 @@ describe('component', () => {
   })
 
   it('warn if functional vapor component not return a block', () => {
+    // @ts-expect-error
     define(() => {
       return () => {}
     }).render()

+ 1 - 2
packages/runtime-vapor/__tests__/componentAttrs.spec.ts

@@ -154,6 +154,7 @@ describe('attribute fallthrough', () => {
       return n0
     })
 
+    // @ts-expect-error
     Child.props = ['foo']
 
     const { host } = define(Hello).render()
@@ -510,7 +511,6 @@ describe('attribute fallthrough', () => {
       render(_ctx, $props, $emit, $attrs, $slots) {
         const n0 = template('<button>hello</button>')() as any
         n0.$evtclick = () => {
-          // @ts-expect-error
           $emit('click', 'custom')
         }
         return n0
@@ -691,7 +691,6 @@ describe('attribute fallthrough', () => {
       },
     })
     const Child = defineVaporComponent({
-      // @ts-expect-error
       components: { GrandChild },
       render(_ctx, $props, $emit, $attrs, $slots) {
         const n0 = template('<div></div>')() as any

+ 1 - 1
packages/runtime-vapor/__tests__/components/Teleport.spec.ts

@@ -84,7 +84,7 @@ describe('renderer: VaporTeleport', () => {
         setup(props) {
           const n0 = template(`<div> </div>`)()
           const x0 = child(n0 as any)
-          renderEffect(() => setText(x0 as any, props.foo))
+          renderEffect(() => setText(x0 as any, String(props.foo)))
           return [n0]
         },
       })

+ 1 - 1
packages/runtime-vapor/__tests__/customElement.spec.ts

@@ -1054,7 +1054,7 @@ describe('defineVaporCustomElement', () => {
         setup() {
           return template('bar')()
         },
-      } as any)
+      } as {})
       const Foo = defineVaporCustomElement({
         styles: [`div { color: red; }`],
         setup() {

+ 0 - 9
packages/runtime-vapor/__tests__/hmr.spec.ts

@@ -68,7 +68,6 @@ describe('hot module replacement', () => {
 
     const Parent = defineVaporComponent({
       __hmrId: parentId,
-      // @ts-expect-error ObjectVaporComponent doesn't have components
       components: { Child },
       setup() {
         const count = ref(0)
@@ -197,7 +196,6 @@ describe('hot module replacement', () => {
 
     const Parent = defineVaporComponent({
       __hmrId: 'parentId',
-      // @ts-expect-error
       components: { Child },
       setup() {
         const toggle = ref(true)
@@ -271,7 +269,6 @@ describe('hot module replacement', () => {
 
     const Parent = defineVaporComponent({
       __hmrId: 'parentId',
-      // @ts-expect-error
       components: { Child },
       setup() {
         const toggle = ref(true)
@@ -349,7 +346,6 @@ describe('hot module replacement', () => {
     createRecord(childId, Child as any)
 
     const Parent = defineVaporComponent({
-      // @ts-expect-error
       components: { Child },
       setup() {
         function onLeave(_: any, done: Function) {
@@ -531,7 +527,6 @@ describe('hot module replacement', () => {
 
     const Parent = defineVaporComponent({
       __hmrId: parentId,
-      // @ts-expect-error
       components: { Child },
       render: compileToFunction(`<Child msg="foo" />`),
     })
@@ -557,7 +552,6 @@ describe('hot module replacement', () => {
 
     const Parent = defineVaporComponent({
       __hmrId: parentId,
-      // @ts-expect-error
       components: { Child },
       render: compileToFunction(`<Child class="test" />`),
     })
@@ -594,13 +588,11 @@ describe('hot module replacement', () => {
       components.push(parentComp)
       if (i === 0) {
         parentComp.render = compileToFunction(`<Child />`)
-        // @ts-expect-error
         parentComp.components = {
           Child,
         }
       } else {
         parentComp.render = compileToFunction(`<Parent />`)
-        // @ts-expect-error
         parentComp.components = {
           Parent: components[i - 1],
         }
@@ -1010,7 +1002,6 @@ describe('hot module replacement', () => {
 
     const { mount, component: Parent } = define({
       __hmrId: parentId,
-      // @ts-expect-error
       components: { Child },
       setup() {
         const msg = ref('root')

+ 1 - 1
packages/runtime-vapor/src/apiCreateFor.ts

@@ -396,7 +396,7 @@ export const createFor = (
       oldBlocks = []
     }
 
-    if (isMounted && frag.updated) frag.updated.forEach(m => m())
+    if (isMounted && frag.onUpdated) frag.onUpdated.forEach(m => m())
     setActiveSub(prevSub)
   }
 

+ 214 - 7
packages/runtime-vapor/src/apiDefineComponent.ts

@@ -1,11 +1,219 @@
-import type { ObjectVaporComponent, VaporComponent } from './component'
-import { extend, isFunction } from '@vue/shared'
+import type { ObjectVaporComponent, VaporComponentInstance } from './component'
+import {
+  type IsKeyValues,
+  type Prettify,
+  extend,
+  isFunction,
+} from '@vue/shared'
+import type {
+  AllowedComponentProps,
+  ComponentCustomProps,
+  ComponentObjectPropsOptions,
+  ComponentTypeEmits,
+  EmitFn,
+  EmitsOptions,
+  EmitsToProps,
+  ExtractDefaultPropTypes,
+  ExtractPropTypes,
+  ReservedProps,
+  TypeEmitsToOptions,
+  VNode,
+} from '@vue/runtime-dom'
+import type { StaticSlots } from './componentSlots'
+import type { Block } from './block'
+
+export type VaporPublicProps = ReservedProps &
+  AllowedComponentProps &
+  ComponentCustomProps
+
+export type VaporRenderResult<T = Block> = VNode | T | VaporRenderResult<T>[]
+
+type VaporComponentInstanceConstructor<T extends VaporComponentInstance> = {
+  __isFragment?: never
+  __isTeleport?: never
+  __isSuspense?: never
+  new (props?: T['props']): T
+}
+
+export type DefineVaporComponent<
+  RuntimePropsOptions = {},
+  RuntimePropsKeys extends string = string,
+  Emits extends EmitsOptions = {},
+  RuntimeEmitsKeys extends string = string,
+  Slots extends StaticSlots = StaticSlots,
+  Exposed extends Record<string, any> = Record<string, any>,
+  TypeBlock extends Block = Block,
+  TypeRefs extends Record<string, unknown> = {},
+  MakeDefaultsOptional extends boolean = true,
+  InferredProps = string extends RuntimePropsKeys
+    ? ComponentObjectPropsOptions extends RuntimePropsOptions
+      ? {}
+      : ExtractPropTypes<RuntimePropsOptions>
+    : { [key in RuntimePropsKeys]?: any },
+  PublicProps = VaporPublicProps,
+  ResolvedProps = InferredProps & EmitsToProps<Emits>,
+  Defaults = ExtractDefaultPropTypes<RuntimePropsOptions>,
+> = VaporComponentInstanceConstructor<
+  VaporComponentInstance<
+    MakeDefaultsOptional extends true
+      ? keyof Defaults extends never
+        ? Prettify<ResolvedProps> & PublicProps
+        : Partial<Defaults> &
+            Omit<Prettify<ResolvedProps> & PublicProps, keyof Defaults>
+      : Prettify<ResolvedProps> & PublicProps,
+    Emits,
+    Slots,
+    Exposed,
+    TypeBlock,
+    TypeRefs
+  >
+> &
+  ObjectVaporComponent<
+    RuntimePropsOptions | RuntimePropsKeys[],
+    Emits,
+    RuntimeEmitsKeys,
+    Slots,
+    Exposed
+  >
+
+export type DefineVaporSetupFnComponent<
+  Props extends Record<string, any> = {},
+  Emits extends EmitsOptions = {},
+  Slots extends StaticSlots = StaticSlots,
+  Exposed extends Record<string, any> = Record<string, any>,
+  TypeBlock extends Block = Block,
+  ResolvedProps extends Record<string, any> = Props &
+    EmitsToProps<Emits> &
+    VaporPublicProps,
+> = new (
+  props?: ResolvedProps,
+) => VaporComponentInstance<ResolvedProps, Emits, Slots, Exposed, TypeBlock>
+
+// overload 1: direct setup function
+// (uses user defined props interface)
+export function defineVaporComponent<
+  Props extends Record<string, any>,
+  Emits extends EmitsOptions = {},
+  RuntimeEmitsKeys extends string = string,
+  Slots extends StaticSlots = StaticSlots,
+  Exposed extends Record<string, any> = Record<string, any>,
+  TypeBlock extends Block = Block,
+>(
+  setup: (
+    props: Props,
+    ctx: {
+      emit: EmitFn<Emits>
+      slots: Slots
+      attrs: Record<string, any>
+      expose: (exposed: Exposed) => void
+    },
+  ) => VaporRenderResult<TypeBlock> | void,
+  extraOptions?: ObjectVaporComponent<
+    (keyof Props)[],
+    Emits,
+    RuntimeEmitsKeys,
+    Slots,
+    Exposed
+  > &
+    ThisType<void>,
+): DefineVaporSetupFnComponent<Props, Emits, Slots, Exposed, TypeBlock>
+export function defineVaporComponent<
+  Props extends Record<string, any>,
+  Emits extends EmitsOptions = {},
+  RuntimeEmitsKeys extends string = string,
+  Slots extends StaticSlots = StaticSlots,
+  Exposed extends Record<string, any> = Record<string, any>,
+  TypeBlock extends Block = Block,
+>(
+  setup: (
+    props: Props,
+    ctx: {
+      emit: EmitFn<Emits>
+      slots: Slots
+      attrs: Record<string, any>
+      expose: (exposed: Exposed) => void
+    },
+  ) => VaporRenderResult<TypeBlock> | void,
+  extraOptions?: ObjectVaporComponent<
+    ComponentObjectPropsOptions<Props>,
+    Emits,
+    RuntimeEmitsKeys,
+    Slots,
+    Exposed
+  > &
+    ThisType<void>,
+): DefineVaporSetupFnComponent<Props, Emits, Slots, Exposed, TypeBlock>
+
+// overload 2: defineVaporComponent with options object, infer props from options
+export function defineVaporComponent<
+  // props
+  TypeProps,
+  RuntimePropsOptions extends
+    ComponentObjectPropsOptions = ComponentObjectPropsOptions,
+  RuntimePropsKeys extends string = string,
+  // emits
+  TypeEmits extends ComponentTypeEmits = {},
+  RuntimeEmitsOptions extends EmitsOptions = {},
+  RuntimeEmitsKeys extends string = string,
+  Slots extends StaticSlots = StaticSlots,
+  Exposed extends Record<string, any> = Record<string, any>,
+  // resolved types
+  ResolvedEmits extends EmitsOptions = {} extends RuntimeEmitsOptions
+    ? TypeEmitsToOptions<TypeEmits>
+    : RuntimeEmitsOptions,
+  InferredProps = IsKeyValues<TypeProps> extends true
+    ? TypeProps
+    : string extends RuntimePropsKeys
+      ? ComponentObjectPropsOptions extends RuntimePropsOptions
+        ? {}
+        : ExtractPropTypes<RuntimePropsOptions>
+      : { [key in RuntimePropsKeys]?: any },
+  TypeRefs extends Record<string, unknown> = {},
+  TypeBlock extends Block = Block,
+>(
+  options: ObjectVaporComponent<
+    RuntimePropsOptions | RuntimePropsKeys[],
+    ResolvedEmits,
+    RuntimeEmitsKeys,
+    Slots,
+    Exposed,
+    TypeBlock,
+    InferredProps
+  > & {
+    /**
+     * @private for language-tools use only
+     */
+    __typeProps?: TypeProps
+    /**
+     * @private for language-tools use only
+     */
+    __typeEmits?: TypeEmits
+    /**
+     * @private for language-tools use only
+     */
+    __typeRefs?: TypeRefs
+    /**
+     * @private for language-tools use only
+     */
+    __typeEl?: TypeBlock
+  } & ThisType<void>,
+): DefineVaporComponent<
+  RuntimePropsOptions,
+  RuntimePropsKeys,
+  ResolvedEmits,
+  RuntimeEmitsKeys,
+  Slots,
+  Exposed extends Block ? Record<string, any> : Exposed,
+  TypeBlock,
+  TypeRefs,
+  // MakeDefaultsOptional - if TypeProps is provided, set to false to use
+  // user props types verbatim
+  unknown extends TypeProps ? true : false,
+  InferredProps
+>
 
 /*! #__NO_SIDE_EFFECTS__ */
-export function defineVaporComponent(
-  comp: VaporComponent,
-  extraOptions?: Omit<ObjectVaporComponent, 'setup'>,
-): VaporComponent {
+export function defineVaporComponent(comp: any, extraOptions?: any) {
   if (isFunction(comp)) {
     // #8236: extend call and options.name access are considered side-effects
     // by Rollup, so we have to wrap it in a pure-annotated IIFE.
@@ -15,7 +223,6 @@ export function defineVaporComponent(
         __vapor: true,
       }))()
   }
-  // TODO type inference
   comp.__vapor = true
   return comp
 }

+ 103 - 53
packages/runtime-vapor/src/component.ts

@@ -1,16 +1,20 @@
 import {
   type AsyncComponentInternalOptions,
   type ComponentInternalOptions,
+  type ComponentObjectPropsOptions,
   type ComponentPropsOptions,
   EffectScope,
   type EmitFn,
   type EmitsOptions,
+  type EmitsToProps,
   ErrorCodes,
+  type ExtractPropTypes,
   type GenericAppContext,
   type GenericComponentInstance,
   type LifecycleHook,
   type NormalizedPropsOptions,
   type ObjectEmitsOptions,
+  type ShallowUnwrapRef,
   type SuspenseBoundary,
   callWithErrorHandling,
   currentInstance,
@@ -51,6 +55,7 @@ import {
 } from '@vue/reactivity'
 import {
   EMPTY_OBJ,
+  type Prettify,
   ShapeFlags,
   hasOwn,
   invokeArrayFns,
@@ -106,42 +111,79 @@ import {
   isLastInsertion,
   resetInsertionState,
 } from './insertionState'
+import type {
+  DefineVaporComponent,
+  VaporRenderResult,
+} from './apiDefineComponent'
 import { DynamicFragment, isFragment } from './fragment'
 import type { VaporElement } from './apiDefineVaporCustomElement'
 import { parentSuspense, setParentSuspense } from './components/Suspense'
 
 export { currentInstance } from '@vue/runtime-dom'
 
-export type VaporComponent = FunctionalVaporComponent | ObjectVaporComponent
-
-export type VaporSetupFn = (
-  props: any,
-  ctx: Pick<VaporComponentInstance, 'slots' | 'attrs' | 'emit' | 'expose'>,
-) => Block | Record<string, any> | undefined
-
-export type FunctionalVaporComponent = VaporSetupFn &
-  Omit<ObjectVaporComponent, 'setup'> & {
+export type VaporComponent =
+  | FunctionalVaporComponent
+  | ObjectVaporComponent
+  | DefineVaporComponent
+
+export type FunctionalVaporComponent<
+  Props = {},
+  Emits extends EmitsOptions = {},
+  Slots extends StaticSlots = StaticSlots,
+  Exposed extends Record<string, any> = Record<string, any>,
+> = ((
+  props: Readonly<Props & EmitsToProps<Emits>>,
+  ctx: {
+    emit: EmitFn<Emits>
+    slots: Slots
+    attrs: Record<string, any>
+    expose: <T extends Record<string, any> = Exposed>(exposed: T) => void
+  },
+) => VaporRenderResult) &
+  Omit<
+    ObjectVaporComponent<ComponentPropsOptions<Props>, Emits, string, Slots>,
+    'setup'
+  > & {
     displayName?: string
   } & SharedInternalOptions
 
-export interface ObjectVaporComponent
-  extends ComponentInternalOptions,
+export interface ObjectVaporComponent<
+  Props = {},
+  Emits extends EmitsOptions = {},
+  RuntimeEmitsKeys extends string = string,
+  Slots extends StaticSlots = StaticSlots,
+  Exposed extends Record<string, any> = Record<string, any>,
+  TypeBlock extends Block = Block,
+  InferredProps = ComponentObjectPropsOptions extends Props
+    ? {}
+    : ExtractPropTypes<Props>,
+> extends ComponentInternalOptions,
     AsyncComponentInternalOptions<ObjectVaporComponent, VaporComponentInstance>,
     SharedInternalOptions {
-  setup?: VaporSetupFn
   inheritAttrs?: boolean
-  props?: ComponentPropsOptions
-  emits?: EmitsOptions
+  props?: Props
+  emits?: Emits | RuntimeEmitsKeys[]
+  slots?: Slots
+  setup?: (
+    props: Readonly<InferredProps>,
+    ctx: {
+      emit: EmitFn<Emits>
+      slots: Slots
+      attrs: Record<string, any>
+      expose: <T extends Record<string, any> = Exposed>(exposed: T) => void
+    },
+  ) => TypeBlock | Exposed | Promise<Exposed> | void
   render?(
-    ctx: any,
-    props?: any,
-    emit?: EmitFn,
-    attrs?: any,
-    slots?: Record<string, VaporSlot>,
-  ): Block
+    ctx: Exposed extends Block ? undefined : ShallowUnwrapRef<Exposed>,
+    props: Readonly<InferredProps>,
+    emit: EmitFn<Emits>,
+    attrs: any,
+    slots: Slots,
+  ): VaporRenderResult<TypeBlock> | void
 
   name?: string
   vapor?: boolean
+  components?: Record<string, VaporComponent>
   /**
    * @internal custom element interception hook
    */
@@ -412,7 +454,7 @@ export function setupComponent(
       )
     }
   } else {
-    handleSetupResult(setupResult, component, instance, setupFn)
+    handleSetupResult(setupResult, component, instance)
   }
 
   setActiveSub(prevSub)
@@ -495,7 +537,15 @@ export const emptyContext: GenericAppContext = {
   provides: /*@__PURE__*/ Object.create(null),
 }
 
-export class VaporComponentInstance implements GenericComponentInstance {
+export class VaporComponentInstance<
+  Props extends Record<string, any> = {},
+  Emits extends EmitsOptions = {},
+  Slots extends StaticSlots = StaticSlots,
+  Exposed extends Record<string, any> = Record<string, any>,
+  TypeBlock extends Block = Block,
+  TypeRefs extends Record<string, any> = Record<string, any>,
+> implements GenericComponentInstance
+{
   vapor: true
   uid: number
   type: VaporComponent
@@ -503,17 +553,17 @@ export class VaporComponentInstance implements GenericComponentInstance {
   parent: GenericComponentInstance | null
   appContext: GenericAppContext
 
-  block: Block
+  block: TypeBlock
   scope: EffectScope
 
   rawProps: RawProps
   rawSlots: RawSlots
 
-  props: Record<string, any>
+  props: Readonly<Props>
   attrs: Record<string, any>
   propsDefaults: Record<string, any> | null
 
-  slots: StaticSlots
+  slots: Slots
 
   scopeId?: string | null
 
@@ -521,15 +571,17 @@ export class VaporComponentInstance implements GenericComponentInstance {
   rawPropsRef?: ShallowRef<any>
   rawSlotsRef?: ShallowRef<any>
 
-  emit: EmitFn
+  emit: EmitFn<Emits>
   emitted: Record<string, boolean> | null
 
-  expose: (exposed: Record<string, any>) => void
-  exposed: Record<string, any> | null
-  exposeProxy: Record<string, any> | null
+  expose: (<T extends Record<string, any> = Exposed>(exposed: T) => void) &
+    // compatible with vdom components
+    string[]
+  exposed: Exposed | null
+  exposeProxy: Prettify<ShallowUnwrapRef<Exposed>> | null
 
   // for useTemplateRef()
-  refs: Record<string, any>
+  refs: TypeRefs
   // for provide / inject
   provides: Record<string, any>
   // for useId
@@ -571,7 +623,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
   sp?: LifecycleHook<() => Promise<unknown>> // LifecycleHooks.SERVER_PREFETCH
 
   // dev only
-  setupState?: Record<string, any>
+  setupState?: Exposed extends Block ? undefined : ShallowUnwrapRef<Exposed>
   devtoolsRawSetupState?: any
   hmrRerender?: () => void
   hmrReload?: (newComp: VaporComponent) => void
@@ -614,9 +666,9 @@ export class VaporComponentInstance implements GenericComponentInstance {
     this.block = null! // to be set
     this.scope = new EffectScope(true)
 
-    this.emit = emit.bind(null, this)
-    this.expose = expose.bind(null, this)
-    this.refs = EMPTY_OBJ
+    this.emit = emit.bind(null, this) as EmitFn<Emits>
+    this.expose = expose.bind(null, this) as any
+    this.refs = EMPTY_OBJ as TypeRefs
     this.emitted = this.exposed = this.exposeProxy = this.propsDefaults = null
 
     // suspense related
@@ -637,22 +689,26 @@ export class VaporComponentInstance implements GenericComponentInstance {
     if (rawProps || comp.props) {
       const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp, once)
       this.attrs = new Proxy(this, attrsHandlers)
-      this.props = comp.props
-        ? new Proxy(this, propsHandlers!)
-        : isFunction(comp)
-          ? this.attrs
-          : EMPTY_OBJ
+      this.props = (
+        comp.props
+          ? new Proxy(this, propsHandlers!)
+          : isFunction(comp)
+            ? this.attrs
+            : EMPTY_OBJ
+      ) as Props
     } else {
-      this.props = this.attrs = EMPTY_OBJ
+      this.props = this.attrs = EMPTY_OBJ as Props
     }
 
     // init slots
     this.rawSlots = rawSlots || EMPTY_OBJ
-    this.slots = rawSlots
-      ? rawSlots.$
-        ? new Proxy(rawSlots, dynamicSlotsProxyHandlers)
-        : rawSlots
-      : EMPTY_OBJ
+    this.slots = (
+      rawSlots
+        ? rawSlots.$
+          ? new Proxy(rawSlots, dynamicSlotsProxyHandlers)
+          : rawSlots
+        : EMPTY_OBJ
+    ) as Slots
 
     this.scopeId = getCurrentScopeId()
 
@@ -802,12 +858,7 @@ export function mountComponent(
   ) {
     const component = instance.type
     instance.suspense.registerDep(instance, setupResult => {
-      handleSetupResult(
-        setupResult,
-        component,
-        instance,
-        isFunction(component) ? component : component.setup,
-      )
+      handleSetupResult(setupResult, component, instance)
       mountComponent(instance, parent, anchor)
     })
     return
@@ -952,7 +1003,6 @@ function handleSetupResult(
   setupResult: any,
   component: VaporComponent,
   instance: VaporComponentInstance,
-  setupFn: VaporSetupFn | undefined,
 ) {
   if (__DEV__) {
     pushWarningContext(instance)
@@ -980,7 +1030,7 @@ function handleSetupResult(
   } else {
     // component has a render function but no setup function
     // (typically components with only a template and no state)
-    if (!setupFn && component.render) {
+    if (setupResult === EMPTY_OBJ && component.render) {
       instance.block = callWithErrorHandling(
         component.render,
         instance,

+ 2 - 2
packages/runtime-vapor/src/components/KeepAlive.ts

@@ -237,14 +237,14 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({
 
     // inject hooks to DynamicFragment to cache components during updates
     const injectKeepAliveHooks = (frag: DynamicFragment) => {
-      ;(frag.beforeTeardown || (frag.beforeTeardown = [])).push(
+      ;(frag.onBeforeTeardown || (frag.onBeforeTeardown = [])).push(
         (oldKey, nodes, scope) => {
           processFragment(frag)
           keptAliveScopes.set(oldKey, scope)
           return true
         },
       )
-      ;(frag.beforeMount || (frag.beforeMount = [])).push(() =>
+      ;(frag.onBeforeMount || (frag.onBeforeMount = [])).push(() =>
         cacheFragment(frag),
       )
       frag.getScope = key => {

+ 4 - 2
packages/runtime-vapor/src/components/Teleport.ts

@@ -103,11 +103,13 @@ export class TeleportFragment extends VaporFragment {
     // it will be called when root fragment changed
     if (this.parentComponent && this.parentComponent.ut) {
       if (isFragment(nodes)) {
-        ;(nodes.updated || (nodes.updated = [])).push(() => updateCssVars(this))
+        ;(nodes.onUpdated || (nodes.onUpdated = [])).push(() =>
+          updateCssVars(this),
+        )
       } else if (isArray(nodes)) {
         nodes.forEach(node => {
           if (isFragment(node)) {
-            ;(node.updated || (node.updated = [])).push(() =>
+            ;(node.onUpdated || (node.onUpdated = [])).push(() =>
               updateCssVars(this),
             )
           }

+ 4 - 5
packages/runtime-vapor/src/components/Transition.ts

@@ -43,8 +43,8 @@ const decorate = (t: typeof VaporTransition) => {
   return t
 }
 
-export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
-  (props, { slots }) => {
+export const VaporTransition: FunctionalVaporComponent<TransitionProps> =
+  /*@__PURE__*/ decorate((props, { slots }) => {
     // wrapped <transition appear>
     let resetDisplay: Function | undefined
     if (
@@ -73,7 +73,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
     }
 
     const children = (slots.default && slots.default()) as any as Block
-    if (!children) return
+    if (!children) return []
 
     const instance = currentInstance! as VaporComponentInstance
     const { mode } = props
@@ -114,8 +114,7 @@ export const VaporTransition: FunctionalVaporComponent = /*@__PURE__*/ decorate(
     }
 
     return children
-  },
-)
+  })
 
 const getTransitionHooksContext = (
   key: string,

+ 12 - 12
packages/runtime-vapor/src/fragment.ts

@@ -60,7 +60,7 @@ export class VaporFragment<T extends Block = Block>
   ) => void
 
   // hooks
-  updated?: ((nodes?: Block) => void)[]
+  onUpdated?: ((nodes?: Block) => void)[]
 
   constructor(nodes: T) {
     this.nodes = nodes
@@ -90,12 +90,12 @@ export class DynamicFragment extends VaporFragment {
   getScope?: (key: any) => EffectScope | undefined
 
   // hooks
-  beforeTeardown?: ((
+  onBeforeTeardown?: ((
     oldKey: any,
     nodes: Block,
     scope: EffectScope,
   ) => boolean)[]
-  beforeMount?: ((newKey: any, nodes: Block, scope: EffectScope) => void)[]
+  onBeforeMount?: ((newKey: any, nodes: Block, scope: EffectScope) => void)[]
 
   constructor(anchorLabel?: string) {
     super([])
@@ -125,8 +125,8 @@ export class DynamicFragment extends VaporFragment {
       let preserveScope = false
       // if any of the hooks returns true the scope will be preserved
       // for kept-alive component
-      if (this.beforeTeardown) {
-        preserveScope = this.beforeTeardown.some(hook =>
+      if (this.onBeforeTeardown) {
+        preserveScope = this.onBeforeTeardown.some(hook =>
           hook(this.current, this.nodes, this.scope!),
         )
       }
@@ -136,7 +136,7 @@ export class DynamicFragment extends VaporFragment {
       const mode = transition && transition.mode
       if (mode) {
         applyTransitionLeaveHooks(this.nodes, transition, () =>
-          this.render(render, transition, parent, instance),
+          this.renderBranch(render, transition, parent, instance),
         )
         parent && remove(this.nodes, parent)
         if (mode === 'out-in') {
@@ -148,7 +148,7 @@ export class DynamicFragment extends VaporFragment {
       }
     }
 
-    this.render(render, transition, parent, instance)
+    this.renderBranch(render, transition, parent, instance)
 
     if (this.fallback) {
       // Find the deepest invalid fragment
@@ -189,7 +189,7 @@ export class DynamicFragment extends VaporFragment {
     if (isHydrating) this.hydrate()
   }
 
-  private render(
+  private renderBranch(
     render: BlockFn | undefined,
     transition: VaporTransitionHooks | undefined,
     parent: ParentNode | null,
@@ -215,8 +215,8 @@ export class DynamicFragment extends VaporFragment {
         this.$transition = applyTransitionHooks(this.nodes, transition)
       }
 
-      if (this.beforeMount) {
-        this.beforeMount.forEach(hook =>
+      if (this.onBeforeMount) {
+        this.onBeforeMount.forEach(hook =>
           hook(this.current, this.nodes, this.scope!),
         )
       }
@@ -240,8 +240,8 @@ export class DynamicFragment extends VaporFragment {
         }
 
         insert(this.nodes, parent, this.anchor)
-        if (this.updated) {
-          this.updated.forEach(hook => hook(this.nodes))
+        if (this.onUpdated) {
+          this.onUpdated.forEach(hook => hook(this.nodes))
         }
       }
     } else {

+ 9 - 2
packages/runtime-vapor/src/index.ts

@@ -1,6 +1,11 @@
 // public APIs
 export { createVaporApp, createVaporSSRApp } from './apiCreateApp'
-export { defineVaporComponent } from './apiDefineComponent'
+export {
+  defineVaporComponent,
+  type DefineVaporComponent,
+  type VaporPublicProps,
+  type VaporRenderResult,
+} from './apiDefineComponent'
 export { defineVaporAsyncComponent } from './apiDefineAsyncComponent'
 export { vaporInteropPlugin } from './vdomInterop'
 export type { VaporDirective } from './directives/custom'
@@ -12,13 +17,15 @@ export {
 } from './apiDefineVaporCustomElement'
 
 // compiler-use only
-export { insert, prepend, remove } from './block'
+export { insert, prepend, remove, type Block } from './block'
 export { setInsertionState } from './insertionState'
 export {
   createComponent,
   createComponentWithFallback,
   createPlainElement,
   isVaporComponent,
+  type FunctionalVaporComponent,
+  type VaporComponentInstance,
 } from './component'
 export { renderEffect } from './renderEffect'
 export { createSlot, withVaporCtx } from './componentSlots'

+ 2 - 2
packages/runtime-vapor/src/vdomInterop.ts

@@ -402,7 +402,7 @@ function createVDOMComponent(
     }
 
     frag.nodes = vnode.el as any
-    if (isMounted && frag.updated) frag.updated.forEach(m => m())
+    if (isMounted && frag.onUpdated) frag.onUpdated.forEach(m => m())
   }
 
   frag.remove = unmount
@@ -469,7 +469,7 @@ function renderVDOMSlot(
       }
     }
 
-    if (isMounted && frag.updated) frag.updated.forEach(m => m())
+    if (isMounted && frag.onUpdated) frag.onUpdated.forEach(m => m())
   }
 
   const render = (parentNode?: ParentNode, anchor?: Node | null) => {