Skip to content
Vincle

Loading…

    JSX types

    The type system that powers Vincle: Renderable, VNode, and the JSX namespace.

    Every HTML and SVG element is typed per element, so a typo is a compile error:

    <dvi clas="card" />
    //^ error: 'dvi' does not exist
    // ^ error: 'clas' does not exist on <div>. Did you mean 'class'?

    The types ship inside @vincle/core, with the attribute names and value unions you know from React. Nothing from React is bundled or imported at runtime.

    Renderable#

    Everything a component may return, and everything the renderers know how to render.

    import type { RawString, VNode } from "@vincle/core";
    export type Renderable =
    | VNode
    | RawString
    | string
    | number
    | bigint
    | boolean
    | null
    | undefined
    | Iterable<Renderable>
    | AsyncIterable<Renderable>
    | Promise<Renderable>;
    Type Example Behaviour
    string "a & b" HTML-escaped and rendered as text
    number / bigint 42, 100n Rendered as text
    boolean / null / undefined true, null Renders nothing
    RawString raw("<b>bold</b>") Passes through verbatim, no escaping
    Promise<Renderable> Promise.resolve(<p>hi</p>) Awaited
    Renderable[] [<a/>, <b/>] Rendered in order, nesting flattened
    Iterable<Renderable> Set, Map.values(), generator Drained, then rendered in order
    AsyncIterable<Renderable> AsyncGenerator Awaited per item, flushed between items

    Anything else, such as a plain object, a Date, or a URL, is rendered through its string form. A function or a symbol is a type error.

    VNode#

    One element built by JSX: a tag, its props, its children. It is exported as a type only: jsx() is the only way to construct one. Use VNode for a value that holds an element; for anything that merely renders, use the wider Renderable.

    Components#

    A component is a plain function, possibly async, returning anything in Renderable. There is no Component<P> type to apply:

    import type { Renderable } from "@vincle/core";
    const Greeting = ({ name }: { name: string }) => <h1>Hello, {name}!</h1>;
    const AsyncGreeting = async ({ id }: { id: string }) => {
    const user = await db.users.find(id);
    return <h1>Hello {user.name}</h1>;
    };
    const Items = () => [<li>a</li>, <li>b</li>];
    const Card = ({ children }: { children?: Renderable }) => <div class="card">{children}</div>;

    To accept children, declare them as Renderable.

    Attributes#

    Attribute names use React's camelCase spelling; the engine maps each one to its HTML name.

    You write The document carries
    className class
    htmlFor for
    tabIndex tabindex
    httpEquiv http-equiv
    strokeWidth stroke-width
    xlinkHref xlink:href
    viewBox viewBox

    HTML names (class, style, data-*, aria-*, http-equiv, xlink:href…) can be written directly too.

    Values may be a string, a number, a boolean, a raw() string, or a promise of any of those. A boolean attribute renders as the bare name: <input disabled /> becomes <input disabled>.

    <a href={resolveUrl(slug)} class={["btn", active && "btn-active"]}>
    link
    </a>

    Event handlers#

    Handlers are strings, not functions, since Vincle emits HTML. A function throws at render time.

    <button onclick="submit()">Click me</button>
    <button onClick="submit()">Both spellings work</button>

    The no-unsafe-event-handlers rule flags inline handlers at the source if you would rather avoid them.

    JSX namespace#

    Vincle declares its own JSX namespace, re-exported by each JSX runtime entry point: TypeScript finds it through the jsxImportSource in your tsconfig.json, so you rarely touch it directly.

    Member Purpose
    JSX.Element What jsx() produces
    JSX.ElementType What may appear as a tag: a string, or a component function
    JSX.IntrinsicElements Attribute types per element, plus open custom elements
    JSX.IntrinsicAttributes Props every element accepts without being attributes (key)
    JSX.ElementChildrenAttribute Names the prop JSX children are written into

    Custom elements#

    Any name containing a hyphen is a custom element: it accepts any attribute, and its children are checked as Renderable.

    <my-widget theme="dark" data-id="7">
    content
    </my-widget>

    To type one, augment JSX.IntrinsicElements. The props must be a type literal, not a named interface (an interface cannot satisfy the custom-element index signature: error TS2411):

    import type { Renderable } from "@vincle/core";
    declare module "@vincle/core/jsx-runtime" {
    namespace JSX {
    interface IntrinsicElements {
    "turbo-frame": { src?: string; target?: string; children?: Renderable };
    }
    }
    }

    CSSProperties#

    Style objects use camelCase properties, converted to kebab-case on output:

    import type { CSSProperties } from "@vincle/core";
    const styles = {
    backgroundColor: "#fff",
    fontSize: "14px",
    "--custom-var": "value", // CSS variables supported
    } satisfies CSSProperties;

    Property names are validated: one carrying :, ; or { is dropped, so keys coming from data cannot smuggle extra declarations into the attribute. Values are CSS-escaped for the same reason: a ; in a value stays inside it.

    ClassValue#

    What the class attribute accepts: a string, or a flat list whose falsy entries are dropped.

    <div class={["card", isActive && "card-active", null]} />
    // => <div class="card card-active">