Skip to content

Dangerous missing type validation on invalidateQueries queryKey #8684

Closed
@williamlmao

Description

@williamlmao

Describe the bug

I just started moving code over to @TkDodo's [Query Options API pattern]](https://tkdodo.eu/blog/the-query-options-api). I noticed something was not invalidating, and it's because I forgot to .queryKey from my query options factory.

I would expect that invalidateQueries would validate that a queryKey needs to be an array. For some reason if queryKey: someValueThatIsOfTypeQueryOptions, it does not throw a type error. You can try out the example below.

Notice how this invalidateQueries is not throwing a type error here. This is problematic, because if you forget to .queryKey, the invalidation will not work.

// No error
queryClient.invalidateQueries({
  // THIS DOES NOT SHOW A TYPE ERROR ON 5.66.9
  queryKey: getTodosOptions,
});

// Should actually be
queryClient.invalidateQueries({
  // THIS DOES NOT SHOW A TYPE ERROR ON 5.66.9
  queryKey: getTodosOptions.queryKey,
});
'use client';

import { queryOptions, useQuery, useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

// Mock API call
const fetchTodos = async (): Promise<Todo[]> => {
  // Simulate API delay
  await new Promise((resolve) => setTimeout(resolve, 1000));

  // Add some randomness to demonstrate invalidation
  const randomSuffix = Math.floor(Math.random() * 100);

  return [
    { id: 1, title: `Learn React Query (${randomSuffix})`, completed: false },
    { id: 2, title: `Build a D&D App (${randomSuffix})`, completed: true },
    { id: 3, title: `Master TypeScript (${randomSuffix})`, completed: false },
  ];
};

const getTodosOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

export default function TestPage() {
  const [filter, setFilter] = useState<'all' | 'completed' | 'incomplete'>(
    'all'
  );
  const queryClient = useQueryClient();
  const {
    data: todos,
    isPending,
    isError,
    isRefetching,
  } = useQuery(getTodosOptions);

  if (isPending || isRefetching) return <div>Loading...</div>;
  if (isError) return <div>Error fetching todos</div>;

  return (
    <div className="p-4 text-black bg-white">
      <h1 className="text-2xl font-bold mb-4">Todo List Example</h1>

      <div className="mb-4">
        <button
          type="button"
          className="mr-2 px-3 py-1 rounded bg-green-500 text-black"
          onClick={() => {
            queryClient.invalidateQueries({
              // THIS DOES NOT SHOW A TYPE ERROR ON 5.66.9
              queryKey: getTodosOptions,
            });
          }}
        >
          Refresh Data
        </button>
        <button
          type="button"
          className={`mr-2 px-3 py-1 rounded ${filter === 'all' ? 'bg-blue-500 text-black' : 'bg-gray-200'}`}
          onClick={() => setFilter('all')}
        >
          All
        </button>
        <button
          type="button"
          className={`mr-2 px-3 py-1 rounded ${filter === 'completed' ? 'bg-blue-500 text-black' : 'bg-gray-200'}`}
          onClick={() => setFilter('completed')}
        >
          Completed
        </button>
        <button
          type="button"
          className={`px-3 py-1 rounded ${filter === 'incomplete' ? 'bg-blue-500 text-black' : 'bg-gray-200'}`}
          onClick={() => setFilter('incomplete')}
        >
          Incomplete
        </button>
      </div>

      <ul className="space-y-2">
        {todos?.map((todo) => (
          <li key={todo.id} className="flex items-center p-2 border rounded">
            <span
              className={`${todo.completed ? 'line-through text-gray-500' : ''}`}
            >
              {todo.title}
            </span>
            <span
              className={`ml-2 px-2 py-1 text-xs rounded ${todo.completed ? 'bg-green-200' : 'bg-yellow-200'}`}
            >
              {todo.completed ? 'Completed' : 'Pending'}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

Your minimal, reproducible example

https://www.typescriptlang.org/play/?#code/OQVwzgpgBAxgNgSwgOwC7ANwCgsILYAOA9gE6pQDeUAjiBCQJ4DyBqCRyYANFOBAIp1GPPoPoMAwohTkAvlABmJInijAAAqgCGnbTADWAehIQtMVAFpa4zLkKlyVPgGVtqaPKUq1Js+my4aPQKZtAAKkQAJkSUWFBQCJEAXFDIIHgARvTY8WyocBApYKgkCMgA5jmwKgQF7slQGUREBTrYsjiGhlAAskQGUACCAAoAkrBacHBYMBzFihCoMAAWEdFgUAC8UFpgDMgwUAAUAJQpw8p4CJAAPGtEANoAugB8W28UcVBdUM74IHAtO4hmMoJEIICGF8tAB3LQIcjICAwqAXFTXCBHI4mMAtABuEBO7ygkFQYXwECIIFQ2IguLgBJ4AEYAAxsk4nALxH6DSKREkqaAkHTRPBIsAbVAxcF4OYlIHQMp4yaJIHsZBfWa6KDC5Ci5wgBQKBAADy2vSBywAdAo4M0SEceparbrRacoAAqKCslmcnDxEyoEAkZBQB5feJURIpJk8PIFFIAAwAMqYQ1AAEqmcxQMSMY4AEgorpUBqNptkJ0TPFmhDqEAaITgkCgsi4EcoCQaACY4wiE1BEwAhEAIOD8rRQAAiADIp0MCARC8WRaXDcaTZXq9U64sGykSnRW+34pGuykAMx9-KFQdO4r0KBhBgECDOGClVjLkt4Msbrc1jU9aNpMLZtl8TztAEWrzOUiz3GALBsHM5rWIwSHqmARyfPEaEMAA0hADApA8wBSuswBPCeNBCAwABiyApAoiwrAh7aVgEEAmsQZBghAIQAuQCggAcyGhmEdKoMMWhwe6OHVNqDzGnA7gkDwpJ0WOqlPOaLhuBANzAJMcDAFAAA+ai1rUe6RKZFnAGUVn1sALxHB2RlTMAXx+vEMHkHhUhIGgumQHmkjSGgpxVH5sSnmCQJaAeURENwHbXMMKCRGU5TUfE1wAKIkMoalpWAWbMUsyzZdR8jbKItFHHBZLJYhrCYX6XwIAoxzpZl2XmRZ1zlSxVUVESgbBqGNxZXiLzJkQWhZRUVorTchgzS8VRdT1YCFcV42LJNUDTQgs17aQCyVf15EpWtG1cjqh3pm5cUnXisCAhKAByWh4BAmwAEQEBYAAsUDuCalgZICAwZOUFgwlV7gAy8HbxDcyxMh9uxgD9f2AxDljdiacCKBwUMtPymSgyj9xQMm1zkPlJq-dZa2Y6jaPHTN2Pfb9-0A9TIMo1z6MZNSUoanF0vgy+Avi6gksA6Lp7wDjeMC3gJAWN2UAECaFgXnrDAWFjygieC-JwxY5QmCgFgAKxsuDXFQzD+jKzLcUcIFBibBQ7qbB8KtxQFEWoFaSoqpECp5kgWHyV70s-GEAASozONOTD5ZnX1MGEvyp0wADqQxPgAmsM+VQPlGYZkwGZQEwX1QA7VoAGzt1aACcIfS3hhHEVATUIRhcy5UnrY+ZPsgdEnqOT5m-E4ss04JSra0K5LC9ezcW8cH3qBy4D+-IJ7k9q3z+MUImWs63rBtGwQJtm1SeoNlARbKapWybNsHkmSgAAfjUNbaGdBHbO0JhYaGZh9CmRSMAa2tstAm27GyYAshExz0nj7RAfsA5EiDiSRYmkVL0COAA4AJwcEyx3knQYUwN6GFPvQ6We8JYH0XkfV8J9OFnz7pfXG-N-a321rrfWhtjamx1G-S2n8KDf0fH-f+TkbKmRAUg+G4CICQJZC7SGMD3YINAfDFBaCMFYNoV7PBCACGBzeBpLSFDgBqPqNQ6xcU2EywkEBGyzDWEb1PofY+ANT7nyTkIjWojJFPxfrIi2H8v7OJIL-f+jk-HuA0aYoxECnb6OgbAgwJitE22FBYlkmDsF91sfYohjjSEpMoRk3cWSaEq28dLUYBxMkQACfwzpd1TqcxljcAEvNhH4wBmAAgoQLBoJFknCgN0wBAKtHgLQBAsQ3XqccPuNxEBQH0ERf2N1I6RHkFEkRANbRcQSO4PAYALAwBkI+YGusmgkHBKk8279IiLMXsdGZOg+5xSudfRMRYzluI-poxASILCoGWObcoq9oHmL0SUqpnivadKWWc+MEAcWjMMMC5AeL2FktBarT6kz-o3zwHAe+kiJHxOgSaDYvz5FQuSlaGF-JNHILtsgHWGCoCIOtgwCEdoYSisqVY6lFKZbLN5fy4BahfGtIbCUjKepsqYOpWtMlSrjqGEQBSjkOK1oAjYUM2a3l2hYCAA

Steps to reproduce

Paste the code above into a react app.

Expected behavior

invalidateQueries throws type error when queryKey is not of type array

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

macOS Sequoia

Tanstack Query adapter

None

TanStack Query version

5.66.9

TypeScript version

5.5.4

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions