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 component | In JSX |
|---|---|
props | the attributes themselves |
emit | onXxx callback props |
slots | the v-slots prop, and JSX children |
exposed | the target type of ref |
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:
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>itemsis still checked asT[]: props are props.iteminside the slot isunknown, and withT extends string | numberit isstring | number— the constraint, not the type argument.- An explicit type argument,
<List<{ id: number }>>, changes the props only:v-slotswas generated during the rewrite, whenThad 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 optionalv-slotsprop.ExposedToProps<Exposed>— the target type ofref, plus an internal marker key.SetupContextToProps<Emits, Slots, Exposed>— emits, slots, and exposed together.
They are exported from vue-jsx:
import type { ExposedToProps, SlotsToProps, SetupContextToProps } from 'vue-jsx'SlotsToProps
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
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.
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:
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,SlotsTypeis not required. Both are accepted, andTreaches the slot parameters — as children or asv-slots.expose: the parameter type is whatrefresolves to,Tincluded.emit: the event props are derived from theemitsignature, soonXxxappears without declaringemitsat all, and the payload keepsT.
defineVaporComponent behaves the same way, except the setup returns a block instead of a render function:
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
| Situation | Use |
|---|---|
| ordinary component with an annotated context | nothing extra, inference is already complete |
generic component, children must see T | SlotsToProps in the props |
generic component, ref must see T | ExposedToProps in the props |
| generic component with emits, slots, and exposed | SetupContextToProps |
| generic component, no helper props in the source | defineComponent / defineVaporComponent |