# TypeScript Patterns for TanStack Query **Type-safe query and mutation patterns** --- ## 1. Basic Type Inference ```tsx type Todo = { id: number title: string completed: boolean } // ✅ Automatic type inference const { data } = useQuery({ queryKey: ['todos'], queryFn: async (): Promise => { const response = await fetch('/api/todos') return response.json() }, }) // data is typed as Todo[] | undefined ``` --- ## 2. Generic Query Hook ```tsx function useEntity( endpoint: string, id: number ) { return useQuery({ queryKey: [endpoint, id], queryFn: async (): Promise => { const response = await fetch(`/api/${endpoint}/${id}`) return response.json() }, }) } // Usage const { data } = useEntity('users', 1) // data: User | undefined ``` --- ## 3. queryOptions with Type Safety ```tsx export const todosQueryOptions = queryOptions({ queryKey: ['todos'], queryFn: async (): Promise => { const response = await fetch('/api/todos') return response.json() }, staleTime: 1000 * 60, }) // Perfect type inference everywhere useQuery(todosQueryOptions) useSuspenseQuery(todosQueryOptions) queryClient.prefetchQuery(todosQueryOptions) ``` --- ## 4. Mutation with Types ```tsx type CreateTodoInput = { title: string } type CreateTodoResponse = Todo const { mutate } = useMutation< CreateTodoResponse, // TData Error, // TError CreateTodoInput, // TVariables { previous?: Todo[] } // TContext >({ mutationFn: async (input) => { const response = await fetch('/api/todos', { method: 'POST', body: JSON.stringify(input), }) return response.json() }, }) // Type-safe mutation mutate({ title: 'New todo' }) ``` --- ## 5. Custom Error Types ```tsx class ApiError extends Error { constructor( message: string, public status: number, public code: string ) { super(message) } } const { data, error } = useQuery({ queryKey: ['todos'], queryFn: async () => { const response = await fetch('/api/todos') if (!response.ok) { throw new ApiError( 'Failed to fetch', response.status, 'FETCH_ERROR' ) } return response.json() }, }) if (error) { // error.status and error.code are typed } ``` --- ## 6. Zod Schema Validation ```tsx import { z } from 'zod' const TodoSchema = z.object({ id: z.number(), title: z.string(), completed: z.boolean(), }) type Todo = z.infer const { data } = useQuery({ queryKey: ['todos'], queryFn: async () => { const response = await fetch('/api/todos') const json = await response.json() return TodoSchema.array().parse(json) // Runtime + compile time safety }, }) ``` --- ## 7. Discriminated Union for Status ```tsx type QueryState = | { status: 'pending'; data: undefined; error: null } | { status: 'error'; data: undefined; error: Error } | { status: 'success'; data: T; error: null } function useTypedQuery( queryKey: string[], queryFn: () => Promise ): QueryState { const { data, status, error } = useQuery({ queryKey, queryFn }) return { status, data: data as any, error: error as any, } } // Usage with exhaustive checking const result = useTypedQuery(['todos'], fetchTodos) switch (result.status) { case 'pending': return case 'error': return // error is typed case 'success': return // data is typed } ``` --- ## 8. Type-Safe Query Keys ```tsx // Define all query keys in one place const queryKeys = { todos: { all: ['todos'] as const, lists: () => [...queryKeys.todos.all, 'list'] as const, list: (filters: TodoFilters) => [...queryKeys.todos.lists(), filters] as const, details: () => [...queryKeys.todos.all, 'detail'] as const, detail: (id: number) => [...queryKeys.todos.details(), id] as const, }, } // Usage useQuery({ queryKey: queryKeys.todos.detail(1), queryFn: () => fetchTodo(1), }) queryClient.invalidateQueries({ queryKey: queryKeys.todos.all }) ``` --- ## 9. Utility Types ```tsx import type { UseQueryResult, UseMutationResult } from '@tanstack/react-query' // Extract query data type type TodosQuery = UseQueryResult type TodoData = TodosQuery['data'] // Todo[] | undefined // Extract mutation types type AddTodoMutation = UseMutationResult< Todo, Error, CreateTodoInput > ``` --- ## 10. Strict Null Checks ```tsx const { data } = useQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id), }) // ❌ TypeScript error if strictNullChecks enabled const title = data.title // ✅ Proper null handling const title = data?.title ?? 'No title' // ✅ Type guard if (data) { const title = data.title // data is Todo, not undefined } ``` --- ## 11. SuspenseQuery Types ```tsx const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos, }) // data is ALWAYS Todo[], never undefined // No need for undefined checks with suspense data.map(todo => todo.title) // ✅ Safe ``` --- ## Best Practices ✅ Always type queryFn return value ✅ Use const assertions for query keys ✅ Leverage queryOptions for reusability ✅ Use Zod for runtime + compile time validation ✅ Enable strict null checks ✅ Create type-safe query key factories ✅ Use custom error types for better error handling