Derive JSX prop types from your components β attributes, events, slots, children β instead of maintaining a parallel declare global block. One helper for most cases, specialized helpers when you need more.
All type helpers are re-exported from elements-kit/jsx-runtime. MaybeReactive also lives in elements-kit/signals.
Custom-element surface
PropsOf<typeof Cls> on a custom-element class returns the full raw JSX surface: attributes, flat properties, prop:*, events, slots, and children β withoutMaybeReactive wrapping. Use this when you need the underlying property/handler types directly (testing, deriving sub-types).
A decorator that makes a class field reactive by automatically wrapping its value in a signal.
The field behaves like a normal property (get/set) but reactivity is tracked under the hood.
Any reads will subscribe to the signal and any writes will trigger updates.
@example
classCounter {
\@reactive() count:number=0;
}
constcounter=newCounter();
counter.count++; // Triggers reactivity
console.log(counter.count); // Subscribes to changes
@remarks β
Equivalent to manually creating a private signal and getter/setter:
Field decorator declaring a slot as a plain property backed by a
Slot
. Reading returns the slot's region (slot.get() β a fragment
to append into any template, elements-kit JSX or not); assigning fills it
(null clears). Values follow native append() semantics β a Node, a
string, or an array of them. Assignment buffers before mount, so consumers
can set content before the element renders.
The getter is EFFECTFUL: it mounts the markers on first read and extracts
current content on later reads (the re-render semantic of
Slot.get
). Read it to PLACE the region, not to inspect it.
Declare the field as
SlotContent
to accept the full native
append() surface (Node | string | array); narrow it to Node when the
slot only ever holds an element.
@example
classCardextendsHTMLElement {
\@slot() header!:SlotContent;
render() {
return <header>{this.header}</header>; // or root.append(card.header)
A class decorator that automatically wires up observedAttributes and attributeChangedCallback
from a static [ATTRIBUTES] map.
The this type inside attribute handlers is automatically inferred from the decorated class.
@example
\@attributes
classMyElementextendsHTMLElement {
static [ATTRIBUTES] = {
count(this:MyElement, value:string|null) {
this.count =Number(value);
},
};
}
attributes
class
classXRange
XRangeextends
var HTMLElement: {
new ():HTMLElement;
prototype:HTMLElement;
}
The HTMLElement interface represents any HTML element. Some elements directly implement this interface, while others implement it via an interface that inherits it.
Field decorator declaring a slot as a plain property backed by a
Slot
. Reading returns the slot's region (slot.get() β a fragment
to append into any template, elements-kit JSX or not); assigning fills it
(null clears). Values follow native append() semantics β a Node, a
string, or an array of them. Assignment buffers before mount, so consumers
can set content before the element renders.
The getter is EFFECTFUL: it mounts the markers on first read and extracts
current content on later reads (the re-render semantic of
Slot.get
). Read it to PLACE the region, not to inspect it.
Declare the field as
SlotContent
to accept the full native
append() surface (Node | string | array); narrow it to Node when the
slot only ever holds an element.
@example
classCardextendsHTMLElement {
\@slot() header!:SlotContent;
render() {
return <header>{this.header}</header>; // or root.append(card.header)
The DOM Node interface is an abstract base class upon which many other DOM API objects are based, thus letting those object types be used similarly and often interchangeably. As an abstract class, there is no such thing as a plain Node object. All objects that implement Node functionality are based on one of its subclasses. Most notable are Document, Element, and DocumentFragment.
A decorator that makes a class field reactive by automatically wrapping its value in a signal.
The field behaves like a normal property (get/set) but reactivity is tracked under the hood.
Any reads will subscribe to the signal and any writes will trigger updates.
@example
classCounter {
\@reactive() count:number=0;
}
constcounter=newCounter();
counter.count++; // Triggers reactivity
console.log(counter.count); // Subscribes to changes
@remarks β
Equivalent to manually creating a private signal and getter/setter:
Attribute keys that also appear on the instance are removed from the attribute slot, so the flat key carries the property type (e.g. number) rather than the handlerβs string type.
For consuming a custom element outside elements-kit JSX (React, Svelte, Vue, vanilla DOM), use the framework-agnostic extractors from elements-kit/custom-elements instead β PropertiesOf, AttributesOf, EventsOf β see Custom Elements.
At the JSX call site, the runtime widens every key to value-or-getter, so parents may pass static values or signals/computed:
<x-rangemin={0} /> // static
<x-rangemin={() =>0} /> // getter
<x-rangemin={signal(0)} /> // signal (a getter)
Plus the namespaced extras (class:, style:, prop:, ref) are layered on every intrinsic element via src/jsx-runtime/properties.ts.
PropsOf<C>
Unified helper. Works on class instances, class constructors, function components, and custom-element constructors.
// 4. Custom-element constructor β the FULL JSX surface
// (attrs, prop:*, on:*,
// named slots, children)
class
classXRange
XRangeextends
var HTMLElement: {
new ():HTMLElement;
prototype:HTMLElement;
}
The HTMLElement interface represents any HTML element. Some elements directly implement this interface, while others implement it via an interface that inherits it.
For HTMLElementinstances, PropsOf<C> returns only the userβs public fields (no DOM surface); the constructor form returns the full custom-element surface (attributes / events / slots). For plain classes, it returns all own fields.
PropsOf accepts components only β a plain prop shape like PropsOf<{ name: string }> is a type error. It returns the raw shapes; the JSX call site widens each key to value-or-getter automatically. Note PropsOf makes every instance field optional (class fields carry initializers, so there is no requiredness signal to preserve).
For class components that accept props via the constructor parameter (class Card { constructor(props: P) }), use TypeScriptβs built-in ConstructorParameters<typeof Card>[0] β the param shape passes through to the JSX call site unchanged.
Props<P>
Declares that every prop accepts a plain value or a reactive source. It is an alias for MaybeReactiveProps<P> under the name function components reach for.
Function-component props are never transformed by the runtime β they arrive exactly as the caller wrote them β so this type is how a component opts in to receiving signals. Read a key with resolve, or hand it to JSX, which subscribes when the value is reactive:
return <p>{props.count}</p>; // live when a signal was passed
}
functionDoubler(props:Props<{ count:number }>) {
return <p>{() =>resolve(props.count) *2}</p>;
}
Omit it and declare plain value types when a component takes static props only β callers then cannot pass a signal, and the component never has to handle one.
ComputedProps<P>
The mirror of Props<P>. Props<P> describes what a caller may pass (value or source); ComputedProps<P> describes what a body reads after converting with computedProps β every key is a Computed<T>.
Turn props into a bag of per-key getters, so a body reads one shape no matter
which form the caller passed. Opt-in: the JSX runtime hands function
components their props untouched.
Every key is callable, including one the caller omitted. A getter is always
truthy, so defaults go on the call: props.excited() ?? "β¦". Keys are
branded sources, so they keep working when forwarded to a child component.
Function props are the limit: a prop taking arguments is rejected, and a
zero-arg one types as its return value. Read those off the raw props.
Read: call with no arguments β returns the current value and
subscribes the active tracking context.
Write: call with a value β updates the signal and schedules
downstream effects if the value changed.
@example
constcount=signal(0);
count(); // β 0 (read)
count(1); // write β effects depending on count will re-run
count(); // β 1
signal } from"elements-kit/signals";
const
constprops:ComputedProps<Unwrap<{
count:Updater<number> &Computed<number>;
label:string;
}>>
props=
computedProps<{
count:Updater<number> &Computed<number>;
label:string;
}>(raw: {
count:Updater<number> &Computed<number>;
label:string;
} &NoArgFnProps<{
count:Updater<number> &Computed<number>;
label:string;
}>):ComputedProps<Unwrap<{
count:Updater<number> &Computed<number>;
label:string;
}>>
Turn props into a bag of per-key getters, so a body reads one shape no matter
which form the caller passed. Opt-in: the JSX runtime hands function
components their props untouched.
Every key is callable, including one the caller omitted. A getter is always
truthy, so defaults go on the call: props.excited() ?? "β¦". Keys are
branded sources, so they keep working when forwarded to a child component.
Function props are the limit: a prop taking arguments is rejected, and a
zero-arg one types as its return value. Read those off the raw props.
Read: call with no arguments β returns the current value and
subscribes the active tracking context.
Write: call with a value β updates the signal and schedules
downstream effects if the value changed.
@example
constcount=signal(0);
count(); // β 0 (read)
count(1); // write β effects depending on count will re-run
count(); // β 1
signal(0),
label: string
label: "n" });
constprops:ComputedProps<Unwrap<{
count:Updater<number> &Computed<number>;
label:string;
}>>
props.
count: () => number
count(); // 0 β subscribes
constprops:ComputedProps<Unwrap<{
count:Updater<number> &Computed<number>;
label:string;
}>>
props.
label: () => string
label(); // "n"
Optional keys lose their ?. The bag hands back a getter for every key, including one the caller omitted, so the getter is never missing β only its result is:
// readonly excited: Computed<boolean | undefined>; // present, not optional
// }
That is what lets you write props.excited() with no ?.. It also means a key is always truthy, so defaults go on the call β props.excited() ?? false, never props.excited ?? false.
computedProps infers its shape from the argument and unwraps reactive keys in the result, so a signal-backed key reads as its value. Function props are the limit of that inference: one taking arguments is rejected, and a zero-arg one types as its return value. See function props.
MaybeReactiveProps<P>
Wrap every prop in MaybeReactive β the same type as Props<P>, under its descriptive name. The JSX checker applies it automatically to intrinsic and custom-element attributes, so you rarely write it there. Name it directly when hand-writing a call-site shape β the main case is a class componentβs constructor param (the shape passes through to the JSX call site verbatim, like Forβs):
Caller-facing wrap: each key accepts a plain value OR a reactive getter.
Name it when typing a call-site shape by hand (e.g. a class component's
constructor param, like For's); the JSX checker applies it to intrinsic
and custom-element props. Function-typed props are wrapped too
(Computed<F> is zero-arg, so TS still picks the handler signature by
arity for inline arrows). Signal<F> must never be added explicitly β its
one-arg Updater half would collapse inline arrow params to implicit any.
Caller-facing wrap: each key accepts a plain value OR a reactive getter.
Name it when typing a call-site shape by hand (e.g. a class component's
constructor param, like For's); the JSX checker applies it to intrinsic
and custom-element props. Function-typed props are wrapped too
(Computed<F> is zero-arg, so TS still picks the handler signature by
arity for inline arrows). Signal<F> must never be added explicitly β its
one-arg Updater half would collapse inline arrow params to implicit any.
MaybeReactiveProps<
typeRaw= {
count:number;
label?:string;
onClick: (e:Event) =>void;
}
Raw>;
// {
// count: MaybeReactive<number>;
// label?: MaybeReactive<string>;
// onClick: MaybeReactive<(e: Event) => void>;
// }
Unlike PropsOf, it preserves required keys.
MaybeReactive<T>
A scalar value or a zero-arg getter that returns the value. Usually a signal or computed.
The getter is only called when the computed value is read and one of
its dependencies has changed since the last evaluation. If nothing has
changed the cached value is returned without re-running getter.
Computed values are read-only; they cannot be set directly.
@param β getter - Pure function deriving a value from other reactive sources.
Receives the previous value as an optional optimisation hint.
@example
consta=signal(1);
constb=signal(2);
constsum=computed(() =>a() +b());
sum(); // β 3
a(10);
sum(); // β 12 (re-evaluated lazily)
computed } from"elements-kit/signals";
importtype {
typeMaybeReactive<T> =T|Computed<T>
A value that may be static or reactive. Accepts a plain T or a
zero-arg getter (() => T) β typically a signal or computed.
Used across the library anywhere a prop or attribute may be bound to
reactive state. Resolve with
Read: call with no arguments β returns the current value and
subscribes the active tracking context.
Write: call with a value β updates the signal and schedules
downstream effects if the value changed.
@example
constcount=signal(0);
count(); // β 0 (read)
count(1); // write β effects depending on count will re-run
count(); // β 1
signal(0);
const
constdouble: () =>number
double=
computed<number>(getter: (previousValue?:number|undefined) => number): () => number
Creates a lazily-evaluated computed value.
The getter is only called when the computed value is read and one of
its dependencies has changed since the last evaluation. If nothing has
changed the cached value is returned without re-running getter.
Computed values are read-only; they cannot be set directly.
@param β getter - Pure function deriving a value from other reactive sources.
Receives the previous value as an optional optimisation hint.
@example
consta=signal(1);
constb=signal(2);
constsum=computed(() =>a() +b());
sum(); // β 3
a(10);
sum(); // β 12 (re-evaluated lazily)
computed(() =>
constcount: () =>number (+1overload)
count() *2);
const
consta:MaybeReactive<number>
a:
typeMaybeReactive<T> =T|Computed<T>
A value that may be static or reactive. Accepts a plain T or a
zero-arg getter (() => T) β typically a signal or computed.
Used across the library anywhere a prop or attribute may be bound to
reactive state. Resolve with