T
TanStack15mo ago
initial-rose

setQueryData changes in v5?

I'm upgrading my project from v4 to v5 of react query today and have just a couple typescript errors with some calls to setQueryData:
Argument of type 'Decimal' is not assignable to parameter of type 'Updater<void | undefined, void | undefined>'
Argument of type 'Decimal' is not assignable to parameter of type 'Updater<void | undefined, void | undefined>'
and
Argument of type 'IPriceInterval[]' is not assignable to parameter of type 'Updater<void | undefined, void | undefined>'
Argument of type 'IPriceInterval[]' is not assignable to parameter of type 'Updater<void | undefined, void | undefined>'
I have dozens of other calls to setQueryData that don't have any error here's an example where p is the Decimal type we're getting an error from:
export const useCurrentMarket = () => {
const queryClient = useQueryClient();
return useQuery({
queryKey: KEY_CURRENT_MARKET,
queryFn: async () => {
const current = await StockHoldingsApi.getCurrent();
current.prices.forEach((p, symbol) =>
queryClient.setQueryData([...KEY_CURRENT_MARKET, symbol], p)
);
return current;
},
staleTime: minutes(1),
});
};
export const useCurrentMarket = () => {
const queryClient = useQueryClient();
return useQuery({
queryKey: KEY_CURRENT_MARKET,
queryFn: async () => {
const current = await StockHoldingsApi.getCurrent();
current.prices.forEach((p, symbol) =>
queryClient.setQueryData([...KEY_CURRENT_MARKET, symbol], p)
);
return current;
},
staleTime: minutes(1),
});
};
Thanks in advance for any insight.
23 Replies
initial-rose
initial-roseOP15mo ago
I should also mention this is only a typing issue, if I ts-ignore the lines it all works
xenogeneic-maroon
xenogeneic-maroon15mo ago
Huh, is it only when you spread into query keys? For some reason this isn't matching the first overload of setQueryData
initial-rose
initial-roseOP15mo ago
nope, I have examples spreading and it working so everywhere in my project that currently works both functionally and type-wise, the signature on setQueryData has updater: unknown, whereas the problem areas, pycharm reports the type as Updater<void | undefined, void | undefined>, not entirely sure why at the moment as an experiment, I tried a refactor and was able to appease the wonderful typing gods, though I'm not quite sure why
export const useCurrentMarket = () => {
const queryClient = useQueryClient();
return useQuery({
queryKey: KEY_CURRENT_MARKET,
queryFn: async () => {
const current = await StockHoldingsApi.getCurrent();
current.prices.forEach((p, symbol) =>
// queryClient.setQueryData([...KEY_CURRENT_MARKET, symbol], p)
setCurrentPrice(queryClient, symbol, p)
);
return current;
},
staleTime: minutes(1),
});
};

const setCurrentPrice = (qc: QueryClient, s: string, p: Decimal) => {
qc.setQueryData([...KEY_CURRENT_MARKET, s], p);
};
export const useCurrentMarket = () => {
const queryClient = useQueryClient();
return useQuery({
queryKey: KEY_CURRENT_MARKET,
queryFn: async () => {
const current = await StockHoldingsApi.getCurrent();
current.prices.forEach((p, symbol) =>
// queryClient.setQueryData([...KEY_CURRENT_MARKET, symbol], p)
setCurrentPrice(queryClient, symbol, p)
);
return current;
},
staleTime: minutes(1),
});
};

const setCurrentPrice = (qc: QueryClient, s: string, p: Decimal) => {
qc.setQueryData([...KEY_CURRENT_MARKET, s], p);
};
conscious-sapphire
conscious-sapphire15mo ago
2 issues bother me 1. current.prices.forEach((p, symbol) => symbol is of type number, not a string 2. If KEY_CURRENT_MARKET is a read-only array it's probably inferred literally. When you spread it, it probably goes to smth more general like a string[] Of course, it's hard to tell what causes the issue without looking at the code and how everything is structured. Small details matter in TS
initial-rose
initial-roseOP15mo ago
1. current.prices is actually a Map<string, Decimal> 2. KEY_CURRENT_MARKET is inferred, simply defined as const KEY_CURRENT_MARKET = ["currentValues"];
conscious-sapphire
conscious-sapphire15mo ago
@SmilyContainer Show me please 1. StockHoldingsApi.getCurrent implementation 2. prices
initial-rose
initial-roseOP15mo ago
public static async getCurrent(): Promise<ICurrentMarket> {
interface IAPIMarketHoliday {
name: string;
exchange: string;
status: string;
open?: string;
close?: string;
}

interface IAPICurrentMarket {
status: string;
prices: Record<string, string>;
holidays: IAPIMarketHoliday[];
}

let c: IAPICurrentMarket = await new GRequest("h/st/cu").fetch();

const parseTime = (t: string) => {
const today = toISODateString(new Date());
return utcToZonedTime(`${today}T${t}.000+00:00`, getBrowserTz());
};

return {
status: c.status,
prices: new Map(
Object.entries(c.prices).map(([s, p]) => [s, new Decimal(p)])
),
holidays: (c.holidays || []).map((h) => ({
...h,
open: h.open ? parseTime(h.open) : undefined,
close: h.close ? parseTime(h.close) : undefined,
})),
};
}
public static async getCurrent(): Promise<ICurrentMarket> {
interface IAPIMarketHoliday {
name: string;
exchange: string;
status: string;
open?: string;
close?: string;
}

interface IAPICurrentMarket {
status: string;
prices: Record<string, string>;
holidays: IAPIMarketHoliday[];
}

let c: IAPICurrentMarket = await new GRequest("h/st/cu").fetch();

const parseTime = (t: string) => {
const today = toISODateString(new Date());
return utcToZonedTime(`${today}T${t}.000+00:00`, getBrowserTz());
};

return {
status: c.status,
prices: new Map(
Object.entries(c.prices).map(([s, p]) => [s, new Decimal(p)])
),
holidays: (c.holidays || []).map((h) => ({
...h,
open: h.open ? parseTime(h.open) : undefined,
close: h.close ? parseTime(h.close) : undefined,
})),
};
}
interface definitions too
export interface IMarketHoliday {
name: string;
exchange: string;
status: string;
open?: Date;
close?: Date;
}

export interface ICurrentMarket {
status: string;
prices: Map<string, Decimal>;
holidays: IMarketHoliday[];
}
export interface IMarketHoliday {
name: string;
exchange: string;
status: string;
open?: Date;
close?: Date;
}

export interface ICurrentMarket {
status: string;
prices: Map<string, Decimal>;
holidays: IMarketHoliday[];
}
thanks for looking and let me know if anything is silly, it's a solo closed source project and I'm more of a backend/infra person so I don't get any feedback on my weird react/ts 😀
conscious-sapphire
conscious-sapphire15mo ago
@SmilyContainer Did you update tanstack/query types when migrating to v5?
initial-rose
initial-roseOP15mo ago
are they separate packages? I've always just had the one package:
 grep -i tanstack package.json
"@tanstack/react-query": "5.49.2",
"@tanstack/react-router": "1.28.1",
"@tanstack/router-devtools": "^1.28.4",
"@tanstack/router-vite-plugin": "^1.28.2",
 grep -i tanstack package.json
"@tanstack/react-query": "5.49.2",
"@tanstack/react-router": "1.28.1",
"@tanstack/router-devtools": "^1.28.4",
"@tanstack/router-vite-plugin": "^1.28.2",
conscious-sapphire
conscious-sapphire15mo ago
I'm trying to break it with different approaches, but I have no succeeded yet 🙂
conscious-sapphire
conscious-sapphire15mo ago
No description
conscious-sapphire
conscious-sapphire15mo ago
F* yeah! I found it.
conscious-sapphire
conscious-sapphire15mo ago
@SmilyContainer You need to add curly braces in your forEach fn and the error will go away.
No description
conscious-sapphire
conscious-sapphire15mo ago
😀
initial-rose
initial-roseOP15mo ago
wow! that's subtle, i'll confirm in a few after undoing my workaround
conscious-sapphire
conscious-sapphire15mo ago
ForEach callback returns void so when you do not wrap it inside the curly braces you return a qc.setQueryData return type. Those 2 combine and break somewhere along the line. Unfortunately, that's the most info I can provide. Query has sophisticated types and it's hard to track down what happens under the hood.
initial-rose
initial-roseOP15mo ago
yea, i took a look at Tk's PR that changed the types on this function in v5 and it's a complicated little PR from a typing perspective. I really appreciate the insight and especially the explanation
conscious-sapphire
conscious-sapphire15mo ago
no problem 🙂
initial-rose
initial-roseOP15mo ago
just sat back down, reverted the workaround, added the curly braces to the 3 instances of the error, all fixed
conscious-sapphire
conscious-sapphire15mo ago
I'm glad we found the cause of the problem. I assume it works like this. 1. Ts knows that the return of the forEach callback is always void 2. Since you didn't provide any Types to setQueryData and you returned it from the callback, TS assigns void to return type of setQueryData 3. Then it backfires going back from the return type all the way up the setQueryData types trying to infer what types of arguments can satisfy the return type void 4. That's how you got this error
initial-rose
initial-roseOP15mo ago
really interesting, should i open up an issue on github for this or is this a lesson for me that "you shouldn't let forEach unnecessarily return something"?
conscious-sapphire
conscious-sapphire15mo ago
It's not necessary problem with types, it's a normal ts behavior when it tries to infer types based on the info it has. It's just not obvious because error makes you think that something wrong with the argument, when in reality problem lies in the return type. It's better just keep in mind that you should not return anything from the foreach cb
initial-rose
initial-roseOP15mo ago
great context for me, I really appreciate the insights, i'll have to remember that and maybe just use for loops more often to avoid the issue

Did you find this page helpful?