Nomad
Nomad2w ago

Weird data type when extending context in fetch adapter

Environment: * pnpm * Vite react SPA * Node v22.11.0 * tRPC server and client version: 11.0.0-rc.828 * Using tRPC with tanstack query (and the new integration approach) I realise that "weird" is a really bad descriptor, but I honestly don't know what else to call it. I'm using the fetch adapter with CF workers and needed cloudflare's env in my context, I added this like such:
// context.ts
import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";

export const createContext = async ({
req,
resHeaders,
env,
}: FetchCreateContextFnOptions & { env: Env }) => {
return { req, resHeaders, env };
};

export type Context = Awaited<ReturnType<typeof createContext>>;

// workers index
import {
FetchCreateContextFnOptions,
fetchRequestHandler,
} from "@trpc/server/adapters/fetch";
import { createContext } from "./context";
import { appRouter } from "./router";

export default {
fetch(request, env) {
const url = new URL(request.url);

if (url.pathname.startsWith("/api")) {
return fetchRequestHandler({
endpoint: "/api",
req: request,
router: appRouter,
// add env to the context along with defaults
createContext: (opts: FetchCreateContextFnOptions) =>
createContext({ ...opts, env }),
});
}

return env.ASSETS.fetch(request);
},
} satisfies ExportedHandler<Env>;
// context.ts
import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";

export const createContext = async ({
req,
resHeaders,
env,
}: FetchCreateContextFnOptions & { env: Env }) => {
return { req, resHeaders, env };
};

export type Context = Awaited<ReturnType<typeof createContext>>;

// workers index
import {
FetchCreateContextFnOptions,
fetchRequestHandler,
} from "@trpc/server/adapters/fetch";
import { createContext } from "./context";
import { appRouter } from "./router";

export default {
fetch(request, env) {
const url = new URL(request.url);

if (url.pathname.startsWith("/api")) {
return fetchRequestHandler({
endpoint: "/api",
req: request,
router: appRouter,
// add env to the context along with defaults
createContext: (opts: FetchCreateContextFnOptions) =>
createContext({ ...opts, env }),
});
}

return env.ASSETS.fetch(request);
},
} satisfies ExportedHandler<Env>;
Now this works wonders, I get access to the cloudflare env in my procedures via context and can use KV bindings from there, very nice! Unfortunately the type from the client is very odd, specifically it considers the data from my query to be of type: (() => never) | undefined As in the following:
const query = useQuery(trpc.kvQuery.queryOptions())
query.data
// ^ (() => never) | undefined
const query = useQuery(trpc.kvQuery.queryOptions())
query.data
// ^ (() => never) | undefined
Now, this purely from a type pespective, the actual value is there during runtime and works fine. But obviously as soon as the code gets type checked it throws a fit because it thinks I'm trying ot access val on that type. Any help is greatly appreciated!
Solution:
Well, in case anyone sees this I managed to figure it out. When starting a new CF project with the react framework template it creates an additional tsconfig (to the three existing ones from vite). This config (tsconfig.worker.json) references the following types: ```json "types": [ "@cloudflare/workers-types/2023-07-01", "./worker-configuration.d.ts",...
Jump to solution
21 Replies
Nomad
NomadOP2w ago
It did occur to me that this could be a tanstack query issue but removing my custom context stuff to get that env in removes the issue (but obviously means I can't use CF features in procedures, so not viable). I've also noticed something odd, not having the query options inlined gives a different (but still not desirable) result, specifically data becomes any instead, as in:
const queryOpts = trpc.kvRead.queryOptions();
const kvQuery = useQuery(queryOpts);
query.data
// ^ any
const queryOpts = trpc.kvRead.queryOptions();
const kvQuery = useQuery(queryOpts);
query.data
// ^ any
BeBoRE
BeBoRE2w ago
Did it work before you did the context stuff, because that all looks good. Have you read the FAQ? https://trpc.io/docs/faq#:~:text=Make%20sure%20you%20have,as%20your%20package.json
FAQ / Troubleshooting | tRPC
Collection of frequently asked questions with ideas on how to troubleshoot & resolve them.
Nomad
NomadOP2w ago
Yup! Removing the context stuff means the types look fine (and at runtime everything continues to work as expected). Gonna have another look over everything in the FAQ, cheers! 😄
BeBoRE
BeBoRE2w ago
Can I ask you why you are exporting the return type of you createContext in your adapter? Makes me believe the type problem is caused by a circular dependency
Nomad
NomadOP2w ago
It's the way it was done in the guide, here: https://trpc.io/docs/server/adapters/fetch#create-the-context I simply copy/pasted and adapted to my needs
Fetch / Edge Runtimes Adapter | tRPC
You can create a tRPC server within any edge runtime that follow the WinterCG, specifically the Minimum Common Web Platform API specification.
Nomad
NomadOP2w ago
I don't believe I'm using it anywhere tho, shall I try to remove it?
BeBoRE
BeBoRE2w ago
You don't use the Context type you are exporting?
Nomad
NomadOP2w ago
Oh nvm I'm reading things weird here 😂 I am of course! It's used when initiating tRPC But yea, the reason it's that exact type is simply down to that being what I copy/pasted from the docs
BeBoRE
BeBoRE2w ago
I believe the problem is that you are defining your Context type in the file where you are consuming the appRouter, while your Context defines your appRouter. This makes the two dependent on each other. If you define your createContext where you are initiating your tRPC you probably won't have the type issues.
Nomad
NomadOP2w ago
Gotcha, I tried moving it all to one file, so I've got this (there's some test procedures for KV in there but that's about it):
import { initTRPC } from "@trpc/server";
import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
import { z } from "zod";

export const createContext = async ({
req,
resHeaders,
env,
}: FetchCreateContextFnOptions & { env: Env }) => {
return { req, resHeaders, env };
};

// type Context = Awaited<ReturnType<typeof createContext>>;

export const t = initTRPC.context<typeof createContext>().create();

export const appRouter = t.router({
kvRead: t.procedure
.input(
z.object({
key: z.string(),
}),
)
.query(async ({ input, ctx }) => {
const val = await ctx.env.medreminder.get(input.key);
return val;
}),
kvWrite: t.procedure
.input(
z.object({
key: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
await ctx.env.medreminder.put(
input.key,
(Math.random() + 1).toString(36).substring(7),
);
}),
});
// export type definition of API
export type AppRouter = typeof appRouter;
import { initTRPC } from "@trpc/server";
import { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
import { z } from "zod";

export const createContext = async ({
req,
resHeaders,
env,
}: FetchCreateContextFnOptions & { env: Env }) => {
return { req, resHeaders, env };
};

// type Context = Awaited<ReturnType<typeof createContext>>;

export const t = initTRPC.context<typeof createContext>().create();

export const appRouter = t.router({
kvRead: t.procedure
.input(
z.object({
key: z.string(),
}),
)
.query(async ({ input, ctx }) => {
const val = await ctx.env.medreminder.get(input.key);
return val;
}),
kvWrite: t.procedure
.input(
z.object({
key: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
await ctx.env.medreminder.put(
input.key,
(Math.random() + 1).toString(36).substring(7),
);
}),
});
// export type definition of API
export type AppRouter = typeof appRouter;
Unfortunately I still get the same issue: https://i.nomad.lol/2025/03/13/SelfishAmericancrayfish.png
Nomad
NomadOP2w ago
(I also tried to straight up use the type of the createContext function just to make sure there's nothing odd with that Context type, but didn't change anything)
BeBoRE
BeBoRE2w ago
Aren't you using the wrong trpc? You should be using the one returned from the useTRPC hook. https://trpc.io/blog/introducing-tanstack-react-query-client#:~:text=const%20trpc%20%3D%20useTRPC()%3B
Introducing the new TanStack React Query integration | tRPC
We are excited to announce the new TanStack React Query integration for tRPC is now available on tRPC's next-release. Compared to our classic React Query Integration it's simpler and more TanStack Query-native, choosing to utilize the QueryOptions and MutationOptions interfaces native to TanStack React Query, instead of wrapping useQuery and use...
Nomad
NomadOP2w ago
According to the docs that's how to use it when set up like I have with a simple SPA: https://trpc.io/docs/client/tanstack-react-query/setup#3b-setup-without-react-context
TanStack React Query | tRPC
TanStack React Query setup
Nomad
NomadOP2w ago
Specifically mentioned in a comment in list item 4:
const trpc = useTRPC(); // use `import { trpc } from './utils/trpc'` if you're using the singleton pattern
const trpc = useTRPC(); // use `import { trpc } from './utils/trpc'` if you're using the singleton pattern
I didn't go for the context provider setup because I don't have SSR and wanted to keep it simple But I can try that if it might help?
BeBoRE
BeBoRE2w ago
Hmm, never used that, don't know if that makes a difference Very weird that it thinks that data is a function
Nomad
NomadOP2w ago
Right!? It'd be one thing if it just went to any or something like that, but the type just doesn't make any sense to me
BeBoRE
BeBoRE2w ago
Yeah this is very strange, idk what could cause this This is also very freaky You are using the correct TS version?
Nomad
NomadOP2w ago
Yea, made sure to upgrade to latest, 5.8.2 It did occur to me tho that it might be because the router is defined outside src (because it's defined with the index for cloudflare workers). And the way vite (and cloudflares starter) does it is to split up the tsconfigs Not sure if it's exactly that but the imports are for sure a bit unconventional with this setup
Nomad
NomadOP2w ago
Because inside the router file the output type is fine: https://i.nomad.lol/2025/03/13/IgnorantConure.png But when it's imported across into my trpc client file it becomes any: https://i.nomad.lol/2025/03/13/GenuineCockatoo.png
Nomad
NomadOP2w ago
(Second image becomes the same if I just hover over the AppRouter type directly)
Solution
Nomad
Nomad2w ago
Well, in case anyone sees this I managed to figure it out. When starting a new CF project with the react framework template it creates an additional tsconfig (to the three existing ones from vite). This config (tsconfig.worker.json) references the following types:
"types": [
"@cloudflare/workers-types/2023-07-01",
"./worker-configuration.d.ts",
"vite/client"
]
"types": [
"@cloudflare/workers-types/2023-07-01",
"./worker-configuration.d.ts",
"vite/client"
]
and the first two also need to be referenced in tsconfig.app.json for the files in the src directory to pick up on it. Adding those two removes the issue and allows the types to be inferred correctly 😄

Did you find this page helpful?