Domphy

Type Safety

Route params are typed

Define the expected params shape in createRoute — TypeScript then enforces them in loaders and components:

import { createRoute } from "@domphy/router"
import { z } from "zod"

const postRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/posts/$postId",
  // Zod schema for path params
  params: {
    parse: (params) => ({
      postId: z.string().transform(Number).parse(params.postId),
    }),
    stringify: (params) => ({
      postId: String(params.postId),
    }),
  },
  loader: ({ params }) => {
    // params.postId: number (Zod transformed it)
    return fetchPost(params.postId)
  },
})

Search params with Zod

Type and validate URL search params:

import { z } from "zod"

const SearchSchema = z.object({
  query:  z.string().default(""),
  page:   z.number().int().min(1).default(1),
  sort:   z.enum(["name", "date", "price"]).default("date"),
  filter: z.array(z.string()).default([]),
})

type SearchParams = z.infer<typeof SearchSchema>

const searchRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/search",
  validateSearch: SearchSchema.parse,
  component: (l) => {
    const match = matches.get(l).find((m) => m.routeId === searchRoute.id)
    const { query, page, sort } = match?.search as SearchParams
    // query: string, page: number, sort: "name"|"date"|"price"
    return {
      div: [
        { h1: (l) => `Results for "${query}"` },
        { p: `Page ${page}` },
      ],
    }
  },
})

Typed loader data

Loader return type flows automatically into the component:

interface Post { id: number; title: string; body: string }

const postRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/posts/$postId",
  loader: async ({ params }) => {
    // TypeScript infers the return type
    const post: Post = await fetchPost(Number(params.postId))
    return { post }
  },
  component: (l) => {
    const match = matches.get(l).find((m) => m.routeId === postRoute.id)
    const { post } = match?.loaderData as { post: Post }
    // post: Post — typed via loader return type

    return {
      article: [
        { h1: post.title },
        { p: post.body },
      ],
    }
  },
})

router.navigate() and Link components are typed against the route tree:

import { createRouter } from "@domphy/router"
import { link } from "@domphy/router/domphy"

const router = createRouter({
  routeTree: rootRoute,
})

// TypeScript error if "to" path is not in the route tree
const NavLink = {
  a: "Blog",
  $: [link({ to: "/blog" })],                          // OK
}

const WrongLink = {
  a: "Nope",
  $: [link({ to: "/nope-does-not-exist" })],           // ✗ TypeScript error
}

// Navigate with typed params
router.navigate({
  to: "/posts/$postId",
  params: { postId: "42" },   // TypeScript requires postId: string
})

Route context typing

Provide a typed context to all routes:

import { createRootRouteWithContext } from "@domphy/router"

interface RouterContext {
  queryClient: QueryClient
  currentUser: User | null
}

const rootRoute = createRootRouteWithContext<RouterContext>()({
  component: () => ({ div: null }),
})

const router = createRouter({
  routeTree: rootRoute,
  context: {
    queryClient,
    currentUser: null,
  },
})

const profileRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/profile",
  beforeLoad: ({ context }) => {
    // context.queryClient: QueryClient — typed
    // context.currentUser: User | null — typed
    if (!context.currentUser) {
      throw redirect({ to: "/login" })
    }
  },
})

Generic route utility types

import type {
  RouteById,
  RouteByPath,
  FullSearchSchema,
  AllParams,
} from "@domphy/router"

// Type of the /posts/$postId route
type PostRoute = RouteByPath<typeof router.routeTree, "/posts/$postId">

// All accumulated search params at a route
type SearchParams = FullSearchSchema<typeof router.routeTree>

// All path params across the route tree
type AllRouteParams = AllParams<typeof router.routeTree>

Strict mode

Enable strict TypeScript mode for the strictest checks — noImplicitAny, exactOptionalPropertyTypes, strictNullChecks:

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

With these settings, route code like params.postId is typed as string (not string | undefined) because the router guarantees param presence — no unnecessary ?. chains needed.