Skip to content
Domphy

TypeScript

Domphy is written in TypeScript and ships full type definitions. This page covers patterns for getting full type safety in UI code.

Element types

import type { DomphyElement } from "@domphy/core"

// A function returning a Domphy element
function heading(text: string): DomphyElement {
  return { h1: text }
}

// A plain element — no function wrapper needed for static content
const footer: DomphyElement = { footer: "© 2025 Acme" }

DomphyElement is the union of all valid element shapes:

  • { tag: content } — element with tag as key
  • string | number | boolean — text node
  • DomphyElement[] — array of elements
  • null | undefined — renders nothing

Typing state

import { toState, RecordState } from "@domphy/core"

// Simple state
const count = toState<number>(0)   // State<number>
const user = toState<User | null>(null)   // State<User | null>

// Record state — typed fields
interface FormData {
  name: string
  email: string
  age: number
}

const form = new RecordState<FormData>({ name: "", email: "", age: 0 })
// form.get("name", l) → string
// form.set("age", 25) — type-checked

Typing listener callbacks

Listener functions receive a Listener parameter:

import type { Listener } from "@domphy/core"

function renderCount(l: Listener): string {
  return `Count: ${count.get(l)}`
}

const Counter = {
  span: (l: Listener) => renderCount(l),
}

In practice, TypeScript infers the Listener type from context — explicit annotation is only needed for extracted functions.

Typing event handlers

DOM event handlers are typed as (event: Event) => void. For specific event types, cast event.target:

const Input = {
  input: null,
  type: "text",
  value: (l) => name.get(l),
  onInput: (event: Event) => {
    const target = event.target as HTMLInputElement
    name.set(target.value)
  },
  onKeyDown: (event: KeyboardEvent) => {
    if (event.key === "Enter") submit()
  },
}

Typing patches

A custom patch is a function returning PartialElement:

import type { PartialElement, ElementNode } from "@domphy/core"

interface TooltipOptions {
  text: string
  placement?: "top" | "bottom" | "left" | "right"
}

function tooltip(options: TooltipOptions): PartialElement {
  return {
    _onMount: (node: ElementNode) => {
      if (node.domElement) node.domElement.title = options.text
    },
    _onRemove: (node: ElementNode) => {
      if (node.domElement) node.domElement.title = ""
    },
  }
}

Typing components

Components are just functions that return DomphyElement. Type their props explicitly:

import type { DomphyElement } from "@domphy/core"

interface CardProps {
  title: string
  body: string
  action?: DomphyElement
}

function Card({ title, body, action }: CardProps): DomphyElement {
  return {
    article: [
      { h2: title },
      { p: body },
      ...(action ? [action] : []),
    ],
  }
}

const MyCard = Card({
  title: "Hello",
  body: "World",
  action: { button: "Click me" },
})

Generic state utilities

import { toState, computed } from "@domphy/core"
import type { State, Listener } from "@domphy/core"

// Generic async state pattern
interface AsyncState<T> {
  data: T | null
  loading: boolean
  error: string | null
}

function createAsyncState<T>(initial: T | null = null): State<AsyncState<T>> {
  return toState<AsyncState<T>>({ data: initial, loading: false, error: null })
}

// Generic selector
function select<T, R>(state: State<T>, fn: (value: T, l: Listener) => R): (l: Listener) => R {
  return (l: Listener) => fn(state.get(l), l)
}

const userName = select(user, (u) => u?.name ?? "Guest")

const Header = {
  span: (l) => userName(l),   // (l: Listener) => string
}

Typing @domphy/ui patches

UI patches are typed — TypeScript will catch invalid prop values:

import { button, inputText, label } from "@domphy/ui"
import type { ElementTone, ElementSize, ElementDensity } from "@domphy/theme"

// button() only accepts { color?: ValueOrState<ThemeColor> }
const btn = button({ color: "primary" })

// Use ElementTone / ElementSize / ElementDensity for typing custom patches:
function myPatch(tone: ElementTone, size: ElementSize, density: ElementDensity): import("@domphy/core").PartialElement {
  return { /* ... */ }
}

Path types for @domphy/form

Form field names are typed with DeepKeys:

import { createForm } from "@domphy/form/domphy"
import type { DeepKeys } from "@domphy/form"

interface UserForm {
  profile: { name: string; bio: string }
  notifications: { email: boolean; sms: boolean }
}

const form = createForm<UserForm>({
  defaultValues: {
    profile: { name: "", bio: "" },
    notifications: { email: true, sms: false },
  },
  onSubmit: ({ value }) => save(value),
})

// DeepKeys<UserForm> = "profile" | "notifications" | "profile.name" | "profile.bio" | "notifications.email" | "notifications.sms"
const nameField = form.field<string>("profile.name", {})   // ✓
const badField  = form.field<string>("profile.age", {})    // ✗ Error: not a valid key

Strict mode

Enable strict TypeScript for the strongest guarantees:

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true
  }
}

With strict: true, Domphy's listener callbacks will correctly require handling T | undefined when indexing arrays.

Type utilities

import type {
  DomphyElement,   // any valid element
  Listener,        // listener callback argument
  State,           // toState return type
  PartialElement,  // patch return type
  ElementNode,     // DOM element wrapper
  AttributeList,   // $-attribute list
  ElementList,     // element list in element.children
} from "@domphy/core"