This episode maximizes TypeScript: strong typing in Query and Table, inference patterns and utility types, type-safe data loading in Router, and runtime schema validation with Zod to close the gap between types and real data.

TanStack was born with TypeScript in its blood: Router, Table, and Query are all designed for type inference. But type safety is only half the journey if data from the server isn't validated — types give guarantees at compile time, schemas give guarantees at runtime.
Episode 17 covers strong typing in Query and Table, inference patterns and utility types, type-safe data loading in Router, and schema validation with Zod. One goal: data entering your app is guaranteed to be well-formed at both levels.
By the end of the episode, your app will reject broken data the moment it arrives — not when a render explodes in production.
TanStack Query infers the data type from the queryFn return value. TanStack Table infers the row type from the generic passed to createColumnHelper:
type Pengguna = { id: number; nama: string }
const columnHelper = createColumnHelper<Pengguna>()
const columns = [
columnHelper.accessor("id", { header: "ID" }),
columnHelper.accessor("nama", { header: "Nama" }),
]createColumnHelper<Pengguna>() binds the row type. A wrong accessor, like columnHelper.accessor("email" when email doesn't exist, is rejected by the compiler immediately. In Query, useQuery without annotations still produces data typed as the queryFn result.
The result types of queries and loaders live at the type level. TanStack Router's utility types let you extract them:
import { useLoaderData } from "@tanstack/react-router"
type DataProyek = Awaited<ReturnType<typeof ambilProyek>>
const proyekRoute = createRoute({
getParentRoute: () => rootRoute,
path: "proyek",
loader: () => ambilProyek(),
component: () => {
const data = useLoaderData({ from: proyekRoute.id })
return <p>{data.nama}</p>
},
})useLoaderData({ from: proyekRoute.id }) gives the exact type of the loader's return value. The Awaited and ReturnType utilities are useful when that type is needed elsewhere without duplicating definitions.
The router infers the search type from validateSearch. With Zod as the validator, the runtime type and the compile-time type become a single source:
import { z } from "zod"
const penggunaRoute = createRoute({
getParentRoute: () => rootRoute,
path: "pengguna",
validateSearch: z.object({
halaman: z.number().default(1),
}),
loader: ({ context, search }) =>
context.queryClient.ensureQueryData({
queryKey: ["pengguna", search.halaman],
queryFn: () => ambilPengguna(search.halaman),
}),
component: PenggunaComponent,
})validateSearch: z.object({ halaman: z.number().default(1) }) makes the router parse search params at runtime while inferring the type of search.halaman as number. The loader, queryKey, and component all share the same type — there's no way to get it wrong.
Query functions are often considered dangerous: server data can be shaped in any way, especially JSON. Parsing at the app's entry point closes this gap:
import { z } from "zod"
const PenggunaSchema = z.object({
id: z.number(),
nama: z.string(),
})
const { data } = useQuery({
queryKey: ["pengguna"],
queryFn: async () => {
const res = await fetch("/api/pengguna")
const json = await res.json()
return PenggunaSchema.parse(json)
},
})PenggunaSchema.parse(json) throws an error if the data shape deviates from the schema. The parse result is typed as the already-inferred PenggunaSchema — your app's data type is guaranteed valid all the way to render. Combining the parse in queryFn with types in useQuery makes the whole chain safe end to end.
Tip
For large payloads, z.lazy and modularly composed schemas keep validation fast and types readable. Don't parse the whole structure in one giant object.
Episode 17 wrapped up TypeScript and schema safety: types inferred automatically in Query and Table, Router utility types extracting loader types, search params validated with Zod, and query functions parsing data at the app's entry point.
Key takeaways:
In the next episode, episode 18, we'll discuss ecosystem integration and patterns — integrating TanStack with React frameworks like Next.js and Remix, using it in vanilla JS and framework-agnostic environments, sharing state between components, and architectural patterns for TanStack-based apps. Your journey extends beyond a single React app!