Skip to content

Component Types

Props, slots, emits, and expose reach the JSX call site through ordinary TypeScript: JSX.LibraryManagedAttributes rewrites the declared component type into the attributes TypeScript then checks. This page covers what that rewrite infers, where it stops for generic components, and the exported types that put the missing part back.

What Gets Rewritten

On the componentIn JSX
propsthe attributes themselves
emitonXxx callback props
slotsthe v-slots prop, and JSX children
exposedthe target type of ref
tsx
const Panel = (props: { step: number }, { slots }: { slots: { default?: (n: number) => any } }) => (
  <div>{slots.default?.(props.step)}</div>
)

export default () => <Panel step={1}>{(n) => <span>{n.toFixed()}</span>}</Panel>

n is number without any language tooling: the slot signature is part of the component type. Type Inference: Props, Refs, And Children walks through the three branches of the rewrite — constructor components, function components, and unknown components.

Generic Props Stop At The Setup Context

Type arguments are inferred from the attributes written on the tag. Everything the component exposes through its second parameter (the setup context), or through the instance type, is resolved before that inference happens, so a type parameter used only there never gets an argument and falls back to its constraint — unknown when there is none:

tsx
const List = <T,>(props: { items: T[] }, { slots }: { slots: { row?: (item: T) => any } }) => (
  <ul>{slots.row?.(props.items[0])}</ul>
)

// `item` is `unknown` in the slot below
export default () => <List items={[{ id: 1 }]}>{{ row: (item) => <li>{item.id}</li> }}</List>
  • items is still checked as T[]: props are props.
  • item inside the slot is unknown, and with T extends string | number it is string | number — the constraint, not the type argument.
  • An explicit type argument, <List<{ id: number }>>, changes the props only: v-slots was generated during the rewrite, when T had already been replaced.

The fix: declare slots, emits, and exposed directly in the props, where TypeScript can infer them from the tag — or let defineComponent / defineVaporComponent declare them for you.

The Props Helpers

Three exported types turn a piece of the setup context into a prop:

  • SlotsToProps<Slots> — the slot signatures as one optional v-slots prop.
  • ExposedToProps<Exposed> — the target type of ref, plus an internal marker key.
  • SetupContextToProps<Emits, Slots, Exposed> — emits, slots, and exposed together.

They are exported from vue-jsx:

ts
import type { ExposedToProps, SlotsToProps, SetupContextToProps } from 'vue-jsx'

SlotsToProps

tsx
import type { SlotsToProps } from 'vue-jsx'

type ListProps<T> = { items: T[] } & SlotsToProps<{ row?: (item: T) => any }>

const List = <T,>(props: ListProps<T>) => <ul>{props.items.length}</ul>

export default () => <List items={[{ id: 1 }]} v-slots={{ row: (item) => <li>{item.id}</li> }} />

item is { id: number } now, because v-slots is one of the attributes TypeScript infers the type argument from. Children work the same way — JSX children are checked against v-slots by ElementChildrenAttribute.

SlotsToProps accepts a plain slot record or Vue's SlotsType, and widens each slot's return type to NodeChild so Virtual DOM and Vapor render results both fit.

ExposedToProps

tsx
import type { ExposedToProps } from 'vue-jsx'

type CardProps<T> = { value: T } & ExposedToProps<{ reset: () => void }>

const Card = <T,>(props: CardProps<T>) => <div>{props.value}</div>

export default () => <Card value={1} ref={(exposed) => exposed?.reset()} />

ExposedToProps<T> adds the internal marker key and types ref as NodeRef<T>, so exposed is { reset: () => void } | null — the same shape a non-generic component gets from ctx.expose().

SetupContextToProps

SetupContextToProps<Emits, Slots, Exposed> is the whole context at once: EmitsToProps, SlotsToProps, and ExposedToProps intersected.

tsx
import type { SetupContextToProps } from 'vue-jsx'

type ListProps<T> = { items: T[] } & SetupContextToProps<
  { change: [value: T] },
  { row?: (item: T) => any },
  { first: T }
>

const List = <T,>(props: ListProps<T>) => <ul>{props.items.length}</ul>

export default () => (
  <List
    items={[1, 2, 3]}
    onChange={(value) => value.toFixed()}
    ref={(exposed) => exposed?.first.toFixed()}
  >
    {{ row: (item) => <li>{item.toFixed()}</li> }}
  </List>
)

defineComponent And defineVaporComponent

vue-jsx exports both: defineComponent for Virtual DOM components, defineVaporComponent for Vapor ones. They take the same setup signature you would write by hand and put emits, slots, and exposed into the props of the returned component, which is the position the attributes are inferred from. A type parameter therefore reaches slot params, ref, and the event props:

tsx
import { defineComponent } from 'vue-jsx'

const List = defineComponent(
  <T,>(
    props: { items: T[] },
    ctx: {
      emit: (e: 'change', v: T) => void
      slots: { row?: (item: T) => any }
      expose: (exposed?: { reset: () => void }) => void
    },
  ) => {
    ctx.expose({ reset: () => {} })
    return () => <ul>{props.items.length}</ul>
  },
)

export default () => (
  <List
    items={[{ id: 1 }]}
    onChange={(value) => value.id}
    ref={(exposed) => exposed?.reset()}
    v-slots={{ row: (item) => <li>{item.id}</li> }}
  />
)

What each part of the context type buys you:

  • slots: a plain slot record is enough, SlotsType is not required. Both are accepted, and T reaches the slot parameters — as children or as v-slots.
  • expose: the parameter type is what ref resolves to, T included.
  • emit: the event props are derived from the emit signature, so onXxx appears without declaring emits at all, and the payload keeps T.

defineVaporComponent behaves the same way, except the setup returns a block instead of a render function:

tsx
import { defineVaporComponent } from 'vue-jsx'

const VaporList = defineVaporComponent(
  <T,>(
    props: { items: T[] },
    ctx: {
      emit: (e: 'change', v: T) => void
      slots: { row?: (item: T) => any }
      expose: (exposed?: { reset: () => void }) => void
    },
  ) => {
    ctx.expose({ reset: () => {} })
    return <ul>{props.items.length}</ul>
  },
)

export default () => (
  <VaporList items={[{ id: 1 }]} ref={(exposed) => exposed?.reset()}>
    {(item) => <li>{item.id}</li>}
  </VaporList>
)

Which To Use

SituationUse
ordinary component with an annotated contextnothing extra, inference is already complete
generic component, children must see TSlotsToProps in the props
generic component, ref must see TExposedToProps in the props
generic component with emits, slots, and exposedSetupContextToProps
generic component, no helper props in the sourcedefineComponent / defineVaporComponent