askill
tanstack-types

tanstack-typesSafety 100Repository

This skill should be used when the user asks about "type-safe React Query", "queryOptions typing", "Zod validation", "TypeScript with TanStack Query", "generic useQuery", "DataTag", "infer query types", or needs guidance on type safety patterns, runtime validation, and TypeScript best practices for TanStack Query.

0 stars
1.2k downloads
Updated 2/8/2026

Package Files

Loading files...
SKILL.md

TanStack Query Type Safety Patterns

This skill provides guidance for achieving type safety in TanStack Query, covering queryOptions, runtime validation with Zod, and TypeScript best practices based on TKDodo's recommendations.

Core Principle: Trust in Types

The foundation of type safety is trust in type definitions. Without trust, TypeScript becomes just a linter that can be silenced.

"To truly leverage the power of TypeScript, there is one thing that you need above all: Trust."

The Anti-Pattern: Manual Generics

Avoid passing type parameters directly to useQuery:

// BAD: Manual generics
const { data } = useQuery<Todo>({
  queryKey: ["todos", id],
  queryFn: () => fetchTodo(id),
});
// data is Todo | undefined, but is it really?
// The generic is just a type assertion in disguise

This violates the "golden rule of generics": For a generic to be useful, it must appear at least twice.

The Pattern: Type the Data Source

Instead of typing the consumer, type the source:

// GOOD: Type the queryFn return
const fetchTodo = async (id: string): Promise<Todo> => {
  const response = await axios.get(`/todos/${id}`);
  return response.data;
};

// Types flow through automatically
const { data } = useQuery({
  queryKey: ["todos", id],
  queryFn: () => fetchTodo(id),
});
// data is Todo | undefined, and we can trust it

The queryOptions Helper

The queryOptions() helper provides compile-time type safety:

import { queryOptions, useQuery } from "@tanstack/react-query";

const todoQueryOptions = (id: string) =>
  queryOptions({
    queryKey: ["todos", id] as const,
    queryFn: async (): Promise<Todo> => {
      const response = await api.get(`/todos/${id}`);
      return response.data;
    },
    staleTime: 5 * 60 * 1000,
  });

// Usage - types are inferred correctly
const { data } = useQuery(todoQueryOptions(id));
// data: Todo | undefined

// TypeScript catches property typos
const bad = queryOptions({
  queryKey: ["todos"],
  queryFn: fetchTodos,
  stallTime: 5000, // Error! Did you mean 'staleTime'?
});

DataTag for getQueryData/setQueryData

queryOptions enables type-safe cache access:

const options = todoQueryOptions(id);

// getQueryData knows the return type
const cachedTodo = queryClient.getQueryData(options.queryKey);
// cachedTodo: Todo | undefined

// setQueryData gets type checking
queryClient.setQueryData(options.queryKey, (old) => {
  // old: Todo | undefined
  return old ? { ...old, completed: true } : old;
});

Runtime Validation with Zod

The network boundary is inherently untrustworthy. Validate responses at runtime:

import { z } from "zod";

// Define schema
const todoSchema = z.object({
  id: z.string(),
  title: z.string(),
  completed: z.boolean(),
  createdAt: z.string().datetime(),
});

type Todo = z.infer<typeof todoSchema>;

// Validate in queryFn
const fetchTodo = async (id: string): Promise<Todo> => {
  const response = await axios.get(`/todos/${id}`);
  return todoSchema.parse(response.data); // Throws if invalid
};

Benefits of Runtime Validation

  1. Catches mismatches early: API changes are caught immediately
  2. Descriptive errors: Zod provides clear error messages
  3. Triggers error state: Invalid data triggers React Query's error handling
  4. Self-documenting: Schema serves as documentation

List Validation

const todosSchema = z.array(todoSchema);

const fetchTodos = async (): Promise<Todo[]> => {
  const response = await axios.get("/todos");
  return todosSchema.parse(response.data);
};

Partial/Optional Fields

const todoSchema = z.object({
  id: z.string(),
  title: z.string(),
  description: z.string().optional(),
  metadata: z.record(z.string()).nullable(),
});

Query Factories with Full Type Safety

Combine queryOptions with factory pattern:

// queries/todos.ts
import { queryOptions } from "@tanstack/react-query";
import { z } from "zod";

const todoSchema = z.object({
  id: z.string(),
  title: z.string(),
  completed: z.boolean(),
});

const todosSchema = z.array(todoSchema);

export type Todo = z.infer<typeof todoSchema>;

export const todoQueries = {
  all: () =>
    queryOptions({
      queryKey: ["todos"] as const,
      queryFn: async () => {
        const res = await api.get("/todos");
        return todosSchema.parse(res.data);
      },
    }),

  detail: (id: string) =>
    queryOptions({
      queryKey: ["todos", "detail", id] as const,
      queryFn: async () => {
        const res = await api.get(`/todos/${id}`);
        return todoSchema.parse(res.data);
      },
      staleTime: 5 * 60 * 1000,
    }),

  byStatus: (status: "active" | "completed") =>
    queryOptions({
      queryKey: ["todos", "status", status] as const,
      queryFn: async () => {
        const res = await api.get(`/todos?status=${status}`);
        return todosSchema.parse(res.data);
      },
    }),
};

Typing Selectors

When using select, type the output properly:

// Basic select - output type inferred
const { data: title } = useQuery({
  ...todoQueryOptions(id),
  select: (data) => data.title, // data: Todo, returns string
});
// title: string | undefined

// With generic for reusable options
const productOptions = <TData = Product>(id: string, select?: (data: Product) => TData) =>
  queryOptions({
    queryKey: ["products", id] as const,
    queryFn: () => fetchProduct(id),
    select,
  });

// Usage
const { data: product } = useQuery(productOptions(id));
// product: Product | undefined

const { data: title } = useQuery(productOptions(id, (p) => p.title));
// title: string | undefined

Typing Mutations

Apply the same patterns to mutations:

const createTodoSchema = z.object({
  title: z.string().min(1),
  description: z.string().optional(),
});

type CreateTodoInput = z.infer<typeof createTodoSchema>;

const useCreateTodo = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (input: CreateTodoInput) => {
      // Validate input
      const validated = createTodoSchema.parse(input);
      const res = await api.post("/todos", validated);
      return todoSchema.parse(res.data);
    },
    onSuccess: (newTodo) => {
      // newTodo is typed as Todo
      queryClient.setQueryData(todoQueries.detail(newTodo.id).queryKey, newTodo);
    },
  });
};

Error Types

Type your errors for better error handling:

// Define error types
interface ApiError {
  code: string;
  message: string;
  details?: Record<string, string[]>;
}

const apiErrorSchema = z.object({
  code: z.string(),
  message: z.string(),
  details: z.record(z.array(z.string())).optional(),
});

// Parse errors in fetch wrapper
const apiFetch = async <T>(url: string, schema: z.ZodType<T>): Promise<T> => {
  const response = await fetch(url);

  if (!response.ok) {
    const error = await response.json();
    throw apiErrorSchema.parse(error);
  }

  return schema.parse(await response.json());
};

// Usage in query
const { data, error } = useQuery({
  queryKey: ["todos"],
  queryFn: () => apiFetch("/todos", todosSchema),
});
// error: ApiError | null

Quick Reference

GoalPattern
Type query dataType the queryFn return, not useQuery generic
Catch typosUse queryOptions() helper
Type-safe cache accessUse queryOptions with getQueryData/setQueryData
Runtime validationParse with Zod in queryFn
Reusable queriesQuery factories with queryOptions
Typed selectorsGeneric parameter on factory function

Common Mistakes

1. Using Generics as Type Assertions

// BAD: This is lying to TypeScript
const { data } = useQuery<Todo>({
  queryKey: ["todo"],
  queryFn: () => fetch("/todo").then((r) => r.json()),
});
// data could be anything at runtime!

// GOOD: Validate at runtime
const { data } = useQuery({
  queryKey: ["todo"],
  queryFn: () =>
    fetch("/todo")
      .then((r) => r.json())
      .then(todoSchema.parse),
});

2. Not Using as const for Query Keys

// BAD: Types are too wide
queryKey: ["todos", id]; // string[]

// GOOD: Exact types preserved
queryKey: ["todos", id] as const; // readonly ['todos', string]

Additional Resources

Reference Files

For detailed patterns and advanced techniques, consult:

  • references/advanced-typing.md - Complex type scenarios

Related Skills

  • tanstack-query - Core concepts, query factories
  • tanstack-mutations - Type-safe mutations
  • tanstack-errors - Typed error handling

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

95/100Analyzed 2/9/2026

An exceptional technical guide for TanStack Query type safety. It provides high-density, accurate patterns including queryOptions, Zod validation, and factory patterns with clear 'Good vs Bad' examples.

100
95
100
95
98

Metadata

Licenseunknown
Version1.0.0
Updated2/8/2026
Publishersalmanrrana

Tags

api