How tRPC Handles Inheritable Generics
Alex, the creator of the tRPC library, discusses how TRPC handles request context without using global variables.
The library uses an
initTRPC object that can be injected with different generics and inherited by everything built on top of it. The "root config" has properties like context and meta that allow for proper inference.
Using the builder pattern helps to avoid using global variables, which can be messy in larger projects.
0:00 I was wondering about how libraries handle things which feel like globals, and I noticed that TRPC has this really cool setup for how it does it. I asked Alex how it works.
0:11 What happens when a request reaches your server, you often want to have a request context. That is where the session lives, or the user, the calling user lives. You want that to be accessed everywhere.
0:25 In order to make sure that the inference just works, you need to have some way of referencing that. You don't want that to be global. The way we do it is that we have an inner TRPC object that we can then inject different generics within that are then inherited by everything that builds on top of that.
0:49 You have inner TRPC, I mean you can hover over that T and you see some crazy stuff. Here you see that we have something called underscore config. That is the root, you see it's called root config. In your root config, you can have a few different properties.
1:07 You have context, meta, errorShape, transformer, etc. We can try now to change the inner TRPC. This is using something called a builder pattern.
1:15 I believe you covered that in your course, but if you call initTRPC.ctx, it's actually a context. It's not called with any actual argument, but you pass in a generic there. You need to pass in an object there. Now if you hover that T variable again, I hope that's going to be different.
1:34 Wow, look at that.
1:37 That's how we build out this root configuration. Then thanks to that now living in that root T object, we can then use that T object to build a procedure from that will know that the context has a user on it. In your greeting query there, you actually use context.
1:57 If I remove some of this stuff and I actually just return the thing, then what I can do is with this, I can say console.log and then ctx.user.id. Whereas, if we remove this and just comment it out, then that will actually be an error because context is just like an object.
2:16 I asked him why he used this approach instead of using a global here.
2:20 I mean, easy answer is that it was the only way I could figure out to make it work, because I didn't want to have globals. One alternative is to have, you can declare interfaces and an interface can override a global interface.
2:37 I could do something like this where you have a TRPC context and we declare that our context is something, exactly what you're declaring here. Then we can have that just work through that as well. Then you couldn't do things like having multiple TRPC servers.
2:54 It feels like an anti-pattern to use a global like this. Instead, we're using the builder pattern and create this concept of a root object.
3:04 There you go. By taking the generic one level higher and using a initTRPC function to create the T that you then build everything out of, you get all the benefits of global inference by letting you pass in your own inference with .context.
3:20 You don't need to use an actual global there, which especially if you're using a mono repo or you want multiple versions of the same library like TRPC, can really pollute everything quickly. A really, really smart solution.