Component utility types, typing props, and declaring custom elements.
pota ships a set of ambient component-utility types in
pota/typescript/jsx/components.d.ts. Once your tsconfig has
"jsxImportSource": "pota", these types are globally available — no
import line needed.
| name | shape | what it's for |
|---|---|---|
Component<P> |
(props: P) => JSX.Element |
any function component taking props P |
ParentComponent<P> |
Component<P & { children?: JSX.Element }> |
component that accepts children — the typical layout/wrapper component |
VoidComponent<P> |
Component<P> |
component that explicitly does not accept children |
FlowComponent<P, C> |
Component<P & { children?: C }> |
flow-control component (Show, For, Switch) where C is a render-callback type |
ComponentType<P> |
Component<P> | (new (props: P) => JSX.ElementClass) |
anything that can be rendered as a component — function or class |
Children<C> |
C | (C | JSX.Element)[] |
mixed list of render callbacks and elements — useful for callback-style children |
ComponentProps<T> |
props of a component function or intrinsic tag | extracts props from a component reference or tag name ('button', typeof MyButton) |
The simplest component is a function that receives a typed props
object. You don't need any of the utility types for this — an inline
object type is enough. Any prop you omit with a default is still
available to callers.
import { render } from 'pota'
function Greeting({ name = 'Quack' }: { name?: string }) {
return <p>Hi {name}</p>
}
render(<Greeting />)
Component<P> is an alias for (props: P) => JSX.Element. Reach for
it when you want the component's type visible at the declaration
(useful for reassignment, defaultProps-style patterns, or handing
the component to a higher-order function). The annotation also forces
the return type to be a JSX element, which catches accidental void
returns early.
import { render } from 'pota'
type GreetingProps = { name?: string }
const Greeting: Component<GreetingProps> = ({ name = 'Quack' }) => (
<p>Hi {name}</p>
)
render(<Greeting name="world" />)
A parent component is one that renders children inside itself —
cards, layouts, providers. ParentComponent<P> adds a
children?: JSX.Element prop on top of P so you don't have to type
it yourself. Callers get autocomplete for children without the
component author reaching for JSX.Element explicitly.
import { render } from 'pota'
const Card: ParentComponent<{ title: string }> = props => (
<section class="card">
<h2>{props.title}</h2>
{props.children}
</section>
)
render(
<Card title="Hello">
<p>inside the card</p>
</Card>,
)
The opposite of ParentComponent: VoidComponent<P> is a component
that must not be passed children. Useful for self-closing leaf
elements like icons, avatars, or form primitives — catching stray
children at compile time prevents bugs where passed content would be
silently ignored.
import { render } from 'pota'
const Avatar: VoidComponent<{ src: string; alt: string }> = props => (
<img
src={props.src}
alt={props.alt}
width="64"
height="64"
/>
)
render(
<Avatar
src="/assets/logo-small.png"
alt="pota logo"
/>,
)
// <Avatar src="…" alt="…">oops</Avatar> ← type error
Flow-control components receive children as a function (or array of
functions) invoked with a value — the pattern <Show/>, <For/>,
<Switch/> use internally. FlowComponent<P, C> lets you type the
callback shape explicitly, so callers get parameter inference on their
render function and can't pass plain JSX where a callback is expected.
Accessor<T> is an ambient type for a reactive read-callback.
import { render, signal } from 'pota'
const Counter: FlowComponent<
{ start?: number },
(count: Accessor<number>) => JSX.Element
> = props => {
const count = signal(props.start ?? 0)
return (
<>
<button on:click={() => count.update(n => n + 1)}>+1</button>
{props.children?.(count.read)}
</>
)
}
render(
<Counter start={10}>{count => <p>clicks: {count}</p>}</Counter>,
)
ComponentType<P> is the union of a function component and a
component class — anything pota knows how to render. Use it for
generic code that works with either flavour: higher-order components,
or prop types that take "a component" without caring which kind.
import { render } from 'pota'
function withBorder<P>(Inner: ComponentType<P>): Component<P> {
return props => (
<div style="border: 1px solid currentColor; padding: .5em">
<Inner {...props} />
</div>
)
}
const Greeting: Component<{ name: string }> = props => (
<p>Hi {props.name}</p>
)
const Bordered = withBorder(Greeting)
render(<Bordered name="world" />)
Children<C> widens a single callback type C into "either one C,
or a mixed array of C and plain JSX elements". It's the shape
<Show/> and <Switch/> accept for their children, so users can
interleave render callbacks with straightforward JSX without extra
wrappers.
import { render, signal } from 'pota'
import { Show } from 'pota/components'
type Renderer<T> = (value: T) => JSX.Element
const MyShow: FlowComponent<
{ when: unknown },
Children<Renderer<unknown>>
> = props => <Show when={props.when}>{props.children}</Show>
const value = signal('hello')
render(
<MyShow when={value.read}>
{v => <p>first callback: {v}</p>}
<hr />
{v => <p>second callback: {v}</p>}
</MyShow>,
)
ComponentProps<T> pulls the props type out of any function component
(typeof Foo) or intrinsic tag ('button', 'a', …). It's how you
build a wrapper that forwards every prop the wrapped thing accepts,
without maintaining a parallel prop list as the underlying type
changes.
import { render } from 'pota'
const PrimaryButton: Component<ComponentProps<'button'>> = props => (
<button {...props} />
)
render(
<PrimaryButton on:click={() => alert('clicked')}>
click me
</PrimaryButton>,
)
Wrap the underlying props with Omit<T, K> when the wrapper should
control a particular prop itself. Callers that try to pass the omitted
key get a compile error, making the intended usage visible at the type
level.
import { render } from 'pota'
type SafeButtonProps = Omit<ComponentProps<'button'>, 'type'>
const SafeButton: Component<SafeButtonProps> = props => (
<button
type="button"
{...props}
/>
)
render(<SafeButton>Safe</SafeButton>)
// <SafeButton type="submit"/> ← type error
Most DOM props are optional by default. To force one required,
subtract it with Omit and add it back through Required<Pick<…>>.
Handy for accessibility-critical props like an <img>'s alt text.
import { render } from 'pota'
type ImgProps = Omit<ComponentProps<'img'>, 'alt' | 'src'> &
Required<Pick<ComponentProps<'img'>, 'alt' | 'src'>>
const Img: Component<ImgProps> = props => <img {...props} />
render(
<Img
src="/assets/logo-small.png"
alt="pota logo"
width="64"
height="64"
/>,
)
// <Img src="…"/> ← type error: alt missing
Behavior helpers in pota are ref factories — plain functions that
return (node) => void, attached via use:ref.
Typing one is the same as typing any other function: parameter types
are checked at the call site, and the return value is whatever
use:ref accepts (a function or nested array of functions).
import { render } from 'pota'
type Tooltip = { text: string }
const tooltip = (opts: Tooltip) => (node: DOMElement) => {
node.setAttribute('title', opts.text)
// …attach behavior; cleanup runs via the surrounding scope
}
function App() {
return <button use:ref={tooltip({ text: 'Hi' })}>hover me</button>
}
render(App)
For an argument that accepts both a plain value and a signal, widen
the parameter with | (() => T) and resolve it inside the body with
withValue.
Declaring a custom element in TypeScript has two parts: tell the
compiler the tag exists, and describe its attributes. You do that by
merging into JSX.IntrinsicElements with a declare global block —
pota's JSX namespace is global, so augment it there rather than
through the pota module. To allow a signal as an attribute value,
widen the type with () => T — that matches pota's runtime acceptance
of function values.
import { render, signal } from 'pota'
declare global {
namespace JSX {
interface IntrinsicElements {
'some-element': JSX.HTMLAttributes<HTMLElement> & {
'some-string'?: string
'some-number'?: number
'some-other'?: number | (() => number)
}
}
}
}
function App() {
const count = signal(0)
return (
<>
<p>count: {count.read}</p>
<some-element
some-string="quack"
some-number={1}
some-other={count.read}
/>
<button on:click={() => count.update(n => n + 1)}>
increment
</button>
</>
)
}
render(App)