TypeScript

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.

Utility types

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)

Examples

Typing props

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

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" />)

ParentComponent

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>,
)

VoidComponent

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

FlowComponent

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

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

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 — forwarding props

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>,
)

Disallowing a prop

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

Requiring a prop

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

Custom use:ref factories

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.

Custom Element

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)