Skip to main content

TypeScript Guide

TanStack Query is written in TypeScript and provides excellent type safety out of the box.

Type Inference

TanStack Query infers types automatically from your query functions:
const { data } = useQuery({
  queryKey: ['repo'],
  queryFn: async () => {
    const response = await fetch('https://api.github.com/repos/TanStack/query')
    return response.json() as Repository
  },
})

// data is automatically typed as Repository | undefined
Use type assertions in your query function to inform TypeScript about the returned data shape.

Explicit Type Parameters

For more control, specify types explicitly:
interface Repository {
  name: string
  description: string
  stargazers_count: number
  forks_count: number
}

const { data, error } = useQuery<
  Repository,  // TQueryFnData - Type returned by queryFn
  Error,       // TError - Type of errors
  Repository,  // TData - Type of data (after select)
  ['repo']     // TQueryKey - Type of queryKey
>({
  queryKey: ['repo'],
  queryFn: async () => {
    const response = await fetch('https://api.github.com/repos/TanStack/query')
    return response.json()
  },
})

// data is Repository | undefined
// error is Error | null
In most cases, you only need to specify the first type parameter (TQueryFnData). The others can be inferred.

Typing Query Keys

Strong typing for query keys helps prevent errors:
type QueryKey = 
  | ['todos']
  | ['todo', number]
  | ['user', string, { includeProjects: boolean }]

const { data } = useQuery<Todo, Error, Todo, ['todo', number]>({
  queryKey: ['todo', todoId],
  queryFn: ({ queryKey }) => {
    const [_key, id] = queryKey // id is typed as number
    return fetchTodo(id)
  },
})

Using queryOptions Helper

The queryOptions helper provides better type inference:
import { queryOptions, useQuery } from '@tanstack/react-query'

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

function postOptions(postId: number) {
  return queryOptions({
    queryKey: ['post', postId],
    queryFn: async (): Promise<Post> => {
      const response = await fetch(`/api/posts/${postId}`)
      return response.json()
    },
    staleTime: 1000 * 60 * 5,
  })
}

// Fully typed, reusable query options
function Post({ postId }: { postId: number }) {
  const { data } = useQuery(postOptions(postId))
  // data is typed as Post | undefined
  
  return <div>{data?.title}</div>
}
Use queryOptions to create reusable, well-typed query configurations that can be shared across components.

Typing Mutations

Mutations support full type safety for variables, data, and errors:
import { useMutation } from '@tanstack/react-query'

interface Todo {
  id: number
  title: string
}

interface CreateTodoVariables {
  title: string
}

function TodoForm() {
  const mutation = useMutation<
    Todo,                    // TData - Mutation response type
    Error,                   // TError - Error type
    CreateTodoVariables,     // TVariables - Variables type
    { previousTodos?: Todo[] } // TContext - Context type
  >({
    mutationFn: async (variables) => {
      // variables is typed as CreateTodoVariables
      const response = await fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(variables),
      })
      return response.json()
    },
    onSuccess: (data, variables, context) => {
      // data is typed as Todo
      // variables is typed as CreateTodoVariables
      // context is typed as { previousTodos?: Todo[] }
      console.log('Created:', data.title)
    },
  })

  return (
    <button
      onClick={() => {
        mutation.mutate({ title: 'New Todo' })
      }}
    >
      Add Todo
    </button>
  )
}

Typing the QueryClient

The QueryClient is fully typed and provides type-safe methods:
import { QueryClient, useQueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient()

function Component() {
  const client = useQueryClient()

  // Type-safe query data access
  const todos = client.getQueryData<Todo[]>(['todos'])
  // todos is Todo[] | undefined

  // Type-safe query data updates
  client.setQueryData<Todo[]>(['todos'], (old) => {
    // old is Todo[] | undefined
    return old ? [...old, newTodo] : [newTodo]
  })
}

Typing Query Results with Initial Data

When providing initial data, the result type changes:
const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  initialData: [] as Todo[],
})

// data is Todo[] (not Todo[] | undefined)
// Because initial data is always present
With initialData, the data property is never undefined, making it easier to work with.

Type-Safe Select

Transform query data with full type safety:
interface Todo {
  id: number
  title: string
  completed: boolean
}

const { data } = useQuery({
  queryKey: ['todos'],
  queryFn: async (): Promise<Todo[]> => {
    const response = await fetch('/api/todos')
    return response.json()
  },
  select: (todos) => {
    // todos is typed as Todo[]
    return todos.filter((todo) => !todo.completed)
  },
})

// data is typed as Todo[] | undefined

Discriminated Unions

Use TypeScript’s discriminated unions with query status:
const result = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
})

if (result.status === 'pending') {
  // TypeScript knows data is undefined here
  return <Spinner />
}

if (result.status === 'error') {
  // TypeScript knows error is defined here
  return <div>Error: {result.error.message}</div>
}

// TypeScript knows data is defined here
return <div>{result.data.length} todos</div>

Generic Query Hook

Create reusable typed query hooks:
import { useQuery, UseQueryOptions, UseQueryResult } from '@tanstack/react-query'

function useTypedQuery<TData, TError = Error>(
  key: unknown[],
  fetcher: () => Promise<TData>,
  options?: Omit<UseQueryOptions<TData, TError>, 'queryKey' | 'queryFn'>
): UseQueryResult<TData, TError> {
  return useQuery<TData, TError>({
    queryKey: key,
    queryFn: fetcher,
    ...options,
  })
}

// Usage
interface User {
  id: string
  name: string
}

function useUser(userId: string) {
  return useTypedQuery<User>(
    ['user', userId],
    () => fetchUser(userId),
    { staleTime: 1000 * 60 * 5 }
  )
}

Infinite Queries

Infinite queries have special type requirements:
import { useInfiniteQuery } from '@tanstack/react-query'

interface Page {
  data: Todo[]
  nextCursor: number | null
}

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery<
  Page,  // TQueryFnData - Single page type
  Error,
  Page,  // TData - After select
  ['todos'],
  number // TPageParam - Page param type
>({
  queryKey: ['todos'],
  queryFn: async ({ pageParam = 0 }) => {
    // pageParam is typed as number
    const response = await fetch(`/api/todos?cursor=${pageParam}`)
    return response.json()
  },
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  initialPageParam: 0,
})

// data.pages is Page[]
Always provide initialPageParam when using useInfiniteQuery to ensure proper typing.

Best Practices

1

Define interfaces for your data

Create TypeScript interfaces for all API responses:
interface User {
  id: string
  name: string
  email: string
}
2

Use queryOptions for reusability

Create typed, reusable query configurations:
const userOptions = (id: string) => queryOptions({
  queryKey: ['user', id],
  queryFn: () => fetchUser(id),
})
3

Let TypeScript infer when possible

Avoid over-specifying types. Let TypeScript infer from your query functions.
4

Use discriminated unions

Take advantage of status checks for better type narrowing.

Common Type Errors

”Type ‘undefined’ is not assignable to type…”

This happens when you forget that data can be undefined:
// ❌ Wrong
const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser })
return <div>{data.name}</div> // Error: data might be undefined

// ✅ Correct
const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser })
if (!data) return null
return <div>{data.name}</div>

// ✅ Also correct with optional chaining
const { data } = useQuery({ queryKey: ['user'], queryFn: fetchUser })
return <div>{data?.name}</div>
Use the status field or provide initialData to avoid undefined checks.

Next Steps

DevTools

Set up DevTools to inspect your typed queries

Essential Concepts

Review core concepts with TypeScript examples

Build docs developers (and LLMs) love