Tribe
Tribe9mo ago

Getting the type of context

Is it possible to get the type on a context object that is passed into a specific TRPC procedure after it is modified by the middleware? If so, whats the best way to go about this? Thanks
6 Replies
BeBoRE
BeBoRE9mo ago
What do you mean by this? What are you trying to do? When you are using a middleware, context the mutation or query receives is modified by that context. Type-inference might need your help however. https://trpc.io/docs/server/middlewares#authorization, in this example you don't pass the context object back to the opts.next because then TypeScript won't infer that the user is not undefined. That's why you pass the user object directly instead. Idk if this answers your question?
Tribe
TribeOP9mo ago
I am basically looking to create a type that gives me the shape of the context after it is extended by the middleware. I am sure it’s possible considering the context is properly typed within mutations and queries. I have just been failing at creating it. So in the example you linked I want a type that tells me the shape of the context including the user that was added into it by the middleware.
BeBoRE
BeBoRE9mo ago
I couldn't find if tRPC exposes that, you could try to infer it like so:
import type { MiddlewareBuilder, ProcedureBuilder } from '@trpc/server/dist/unstable-core-do-not-import';

type inferMiddlewareContext<TMiddleware> = TMiddleware extends MiddlewareBuilder<infer TCtx, any, any, any> ? TCtx : never;
type inferProcedureContext<TProcedure> = TProcedure extends ProcedureBuilder<infer TCtx, any, any, any, any, any, any> ? TCtx : never;
import type { MiddlewareBuilder, ProcedureBuilder } from '@trpc/server/dist/unstable-core-do-not-import';

type inferMiddlewareContext<TMiddleware> = TMiddleware extends MiddlewareBuilder<infer TCtx, any, any, any> ? TCtx : never;
type inferProcedureContext<TProcedure> = TProcedure extends ProcedureBuilder<infer TCtx, any, any, any, any, any, any> ? TCtx : never;
But this could break when tRPC updates, so use at your own risk You may want to open an issue if you want this type of inference out of the box
Tribe
TribeOP9mo ago
Much appreciated. I’ll see if that works just for curiosity sake but you’re probably right, might be safer to just open a ticket.
BeBoRE
BeBoRE9mo ago
I just tested my solution, it doesn't work. The first generic is the incoming context, the third generic is the overwritten context, in the builder these get combined for the procedure, but that is an internal type util that I cannot make use of, you'd have to copy the Overwrite generic yourself if you want to combine the two. You can use:
type inferMiddlewareContext<TMiddleware> = TMiddleware extends MiddlewareBuilder<any, any, infer TContextOverwrite, any> ? TContextOverwrite : never;
type inferProcedureContext<TProcedure> = TProcedure extends ProcedureBuilder<any, any, infer TContextOverwrite, any, any, any, any> ? TContextOverwrite : never;
type inferMiddlewareContext<TMiddleware> = TMiddleware extends MiddlewareBuilder<any, any, infer TContextOverwrite, any> ? TContextOverwrite : never;
type inferProcedureContext<TProcedure> = TProcedure extends ProcedureBuilder<any, any, infer TContextOverwrite, any, any, any, any> ? TContextOverwrite : never;
And just make sure that the overwritten context is the same as the incoming context (for example)
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.dbUser || !ctx.session || !ctx.discordUser) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
...ctx, // Makes sure the overwritten context is the same as the incoming context
ctx: {
// infers the as non-nullable
dbUser: { ...ctx.dbUser },
session: { ...ctx.session},
discordUser: { ...ctx.discordUser },
},
});
});
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.dbUser || !ctx.session || !ctx.discordUser) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
...ctx, // Makes sure the overwritten context is the same as the incoming context
ctx: {
// infers the as non-nullable
dbUser: { ...ctx.dbUser },
session: { ...ctx.session},
discordUser: { ...ctx.discordUser },
},
});
});
If you don't want to always make sure that they are the same you can copy the Overwrite util.
export type WithoutIndexSignature<TObj> = {
[K in keyof TObj as string extends K
? never
: number extends K
? never
: K]: TObj[K];
};

export type Overwrite<TType, TWith> = TWith extends any
? TType extends object
? {
[K in // Exclude index signature from keys
| keyof WithoutIndexSignature<TType>
| keyof WithoutIndexSignature<TWith>]: K extends keyof TWith
? TWith[K]
: K extends keyof TType
? TType[K]
: never;
} & (string extends keyof TWith // Handle cases with an index signature
? { [key: string]: TWith[string] }
: number extends keyof TWith
? { [key: number]: TWith[number] }
: // eslint-disable-next-line @typescript-eslint/ban-types
{})
: TWith
: never;
export type WithoutIndexSignature<TObj> = {
[K in keyof TObj as string extends K
? never
: number extends K
? never
: K]: TObj[K];
};

export type Overwrite<TType, TWith> = TWith extends any
? TType extends object
? {
[K in // Exclude index signature from keys
| keyof WithoutIndexSignature<TType>
| keyof WithoutIndexSignature<TWith>]: K extends keyof TWith
? TWith[K]
: K extends keyof TType
? TType[K]
: never;
} & (string extends keyof TWith // Handle cases with an index signature
? { [key: string]: TWith[string] }
: number extends keyof TWith
? { [key: number]: TWith[number] }
: // eslint-disable-next-line @typescript-eslint/ban-types
{})
: TWith
: never;
And change the infer utilities to:
type inferMiddlewareContext<TMiddleware> = TMiddleware extends MiddlewareBuilder<infer $Context, any, infer $ContextOverwrite, any> ? Overwrite<$Context, $ContextOverwrite> : never;
type inferProcedureContext<TProcedure> = TProcedure extends ProcedureBuilder<infer $Context, any, infer $ContextOverwrite, any, any, any, any> ? Overwrite<$Context, $ContextOverwrite> : never;
type inferMiddlewareContext<TMiddleware> = TMiddleware extends MiddlewareBuilder<infer $Context, any, infer $ContextOverwrite, any> ? Overwrite<$Context, $ContextOverwrite> : never;
type inferProcedureContext<TProcedure> = TProcedure extends ProcedureBuilder<infer $Context, any, infer $ContextOverwrite, any, any, any, any> ? Overwrite<$Context, $ContextOverwrite> : never;
Tribe
TribeOP9mo ago
Thank you for diving this deep into this, just looking into what you provided now. So since in some of my middlewares I am adding new properties to the context, I dont need the incoming context to match the outgoing so I wanna go with the second Overwrite option I believe