Learn TanStack - TypeScript & Schema Safety
Episode 17 of 24

Learn TanStack - TypeScript & Schema Safety

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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.

Strong Typing in Query and Table

Automatic Inference Without Annotations

TanStack Query infers the data type from the queryFn return value. TanStack Table infers the row type from the generic passed to createColumnHelper:

JSTyping kolom dan baris
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.

Inference and Utility Types

Using Already-Inferred Types

The result types of queries and loaders live at the type level. TanStack Router's utility types let you extract them:

JSMengekstrak tipe dari route
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.

Type-safe Data Loading in Router

Validated Search Params and Loaders

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:

JSValidasi search params dengan Zod
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.

Enforcing Schema Validation with Zod

Guarding the Boundary Between API and App

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:

JSSchema Zod untuk validasi data
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.

Conclusion

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:

  • Query and Table infer types without manual annotations.
  • createColumnHelper binds the row type and validates accessors.
  • useLoaderData gives the exact type of the loader result.
  • validateSearch with Zod connects runtime and compile types.
  • Zod.parse in queryFn rejects broken data from the start.
  • Combining types and schemas closes the gap between compile and runtime.

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!

Learn TanStack - TypeScript & Schema Safety | Learn TanStack