TypeScript
Automatic type inference
Query data types are inferred from queryFn's return type — no manual annotation needed:
import { QueryClient } from "@domphy/query"
import { createQuery } from "@domphy/query/domphy"
interface User { id: string; name: string; email: string }
async function fetchUser(id: string): Promise<User> {
return fetch(`/api/users/${id}`).then((r) => r.json())
}
const queryClient = new QueryClient()
const user = createQuery(queryClient, {
queryKey: () => ["user", userId],
queryFn: () => fetchUser(userId),
// No type annotation needed — data is inferred as User
})
// user.data(l) → User | undefined ✓ (correctly typed)
const name = user.data()?.name // string | undefinedqueryOptions helper
Define reusable, fully-typed query configs with the queryOptions helper:
import { QueryClient, queryOptions } from "@domphy/query"
import { createQuery } from "@domphy/query/domphy"
const queryClient = new QueryClient()
const userQueryOptions = (id: string) => queryOptions({
queryKey: ["user", id] as const,
queryFn: () => fetchUser(id),
staleTime: 60_000,
})
// Use anywhere — type is preserved
const user = createQuery(queryClient, userQueryOptions(userId))
queryClient.prefetchQuery(userQueryOptions(nextUserId))
queryClient.getQueryData(userQueryOptions(userId).queryKey) // → User | undefinedTyped query keys
Define query key factories for type-safe cache operations:
export const queryKeys = {
users: {
all: () => ["users"] as const,
list: (filters: UserFilters) => ["users", "list", filters] as const,
detail: (id: string) => ["users", "detail", id] as const,
},
posts: {
all: () => ["posts"] as const,
byTag: (tag: string) => ["posts", "tag", tag] as const,
},
}
// Strongly typed invalidation
client.invalidateQueries({ queryKey: queryKeys.users.all() })
client.getQueryData<User>(queryKeys.users.detail(userId))Error types
By default, query.error(l) is typed as Error | null. Narrow to your API error type:
import { QueryClient } from "@domphy/query"
import { createQuery } from "@domphy/query/domphy"
interface ApiError {
status: number
message: string
code: string
}
const queryClient = new QueryClient()
const user = createQuery<User, ApiError>(queryClient, {
queryKey: () => ["user"],
queryFn: async () => {
const res = await fetch("/api/user")
if (!res.ok) throw await res.json() as ApiError
return res.json() as Promise<User>
},
})
// user.error(l) → ApiError | null (fully typed)
const errorMsg = user.error()?.message
const statusCode = user.error()?.statusMutation types
Type the mutation variables, data, and error:
import { QueryClient } from "@domphy/query"
import { createMutation } from "@domphy/query/domphy"
interface CreatePostInput { title: string; body: string }
interface Post { id: string; title: string; body: string }
const queryClient = new QueryClient()
const createPost = createMutation<Post, ApiError, CreatePostInput>(queryClient, {
mutationFn: (input) => api.post<Post>("/posts", input),
onSuccess: (data) => {
// data: Post ✓
queryClient.setQueryData<Post[]>(["posts"], (old = []) => [...old, data])
},
onError: (error) => {
// error: ApiError ✓
console.error(error.code, error.message)
},
})
// mutate is typed
createPost.mutate({ title: "Hello", body: "World" })
createPost.mutate({ title: 42 }) // ✗ Error: title must be stringInfinite query types
import { QueryClient } from "@domphy/query"
import { createInfiniteQuery } from "@domphy/query/domphy"
interface PostPage { posts: Post[]; nextCursor: string | null }
const queryClient = new QueryClient()
const feed = createInfiniteQuery<PostPage, ApiError, PostPage, string[], string>(queryClient, {
queryKey: () => ["feed"],
queryFn: ({ pageParam }) => fetchFeedPage(pageParam),
initialPageParam: "",
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
})
// feed.data(l)?.pages → PostPage[] ✓
const allPosts = feed.data()?.pages.flatMap((p) => p.posts) ?? []QueryClient typed methods
// Typed getQueryData
const user = client.getQueryData<User>(["user", id]) // User | undefined
// Typed setQueryData
client.setQueryData<User[]>(["users"], (old = []) => [...old, newUser])
// Typed cancelQueries
await client.cancelQueries({ queryKey: ["user"] })
// Typed invalidation with partial key matching
client.invalidateQueries({ queryKey: ["users"], exact: false })
// Invalidates ["users"], ["users", "list", ...], ["users", "detail", ...]Discriminated union for async state
When you want exhaustive type-narrowing of query states:
import type { UseQueryResult } from "@domphy/query"
function renderQuery<T>(query: UseQueryResult<T, Error>) {
if (query.status === "pending") return { div: "Loading…" }
if (query.status === "error") return { div: `Error: ${query.error.message}` }
// TypeScript now knows query.data: T (non-nullable)
return renderData(query.data)
}Type-safe mutations with form
Integrate @domphy/form and @domphy/query with shared types:
import { QueryClient } from "@domphy/query"
import { createForm } from "@domphy/form/domphy"
import { createMutation } from "@domphy/query/domphy"
interface LoginInput { email: string; password: string }
interface LoginResult { token: string; user: User }
const queryClient = new QueryClient()
const loginMutation = createMutation<LoginResult, ApiError, LoginInput>(queryClient, {
mutationFn: (input) => api.post<LoginResult>("/auth/login", input),
})
const loginForm = createForm<LoginInput>({
defaultValues: { email: "", password: "" },
onSubmit: async ({ value }) => {
await loginMutation.mutateAsync(value)
// loginMutation.data() → LoginResult (typed)
},
})