Exploring Generics in React Query with Tanner Linsley
Tanner Linsley, co-founder of Nozzle and the creator of the TanStack collection of open-source libraries, shares how building React Query led him to embrace TypeScript.
In React Query, generics are used to allow for flexibility in the types of data that can be returned from queries. However, Tanner
Transcript
Matt Pocock: 0:00 What's up, Wizards? I'm here with Tanner Linsley, a legend, a friend, burger introducer, Utahn, extremely knowledgeable person, and someone who has come to TypeScript late on after being a denier of TypeScript, let's say, or a heathen of Typescript...
Tanner Linsley: 0:25 We'll say procrastinator.
Matt: 0:28 Yeah, but has now gathered an enormous amount of knowledge about TypeScript and has some of the most complex, coolest TypeScript libraries going. Tanner, why don't you introduce yourself?
Tanner: 0:39 My name is Tanner Linsley. I am from Utah. I love burgers, and Matt knows that. I introduced Matt to one of my favorite burgers in Utah last time he was here.
0:52 I co-founded a startup called Nozzle about eight, nine years ago. We are an SEO analytics platform, to put it simply.
1:06 Throughout that process, and on the side as well, I maintain TanStack, which is a very cringe name, I guess is what people say. [laughs] It's just a collection of my open source libraries. A few of those that you might have heard of are React Query, now TanStack Query. I still can't say it. I have a hard time saying TanStack Query.
Matt: 1:33 TanStack Query?
Tanner: 1:33 Yeah. I just really don't like to say my own name.
Matt: 1:35 [indecipherable] .
Tanner: 1:35 [laughs] There's Query and there's Table. I'm working on Router now. We have Virtual, which is virtualization for UI elements. It's a lot of fun. There's a very healthy, sometimes unhealthy balance between my open source work and my startup.
1:58 For the most part, everything that I've built in for TanStack we've needed because of some challenge we saw at Nozzle.
Matt: 2:08 What I find really interesting about your work is that it basically always intersects at the point where you need a generic for something. You have a fetch function. The thing that you're going to be fetching is going to need a generic, and it's going to wrap that returns and nice methods over at Tables.
2:25 You're going to need a generic for the row, a virtualized list. You're going to need a generic for the row again. Tell me your story with how you went from...Because a lot of these libraries were written in JavaScript first, and you ported them over.
Tanner: 2:41 I originally wrote all of these libraries just in JavaScript. They worked fine. Really, what it came down to was...First of all, I put TypeScript off for a really long time, mostly because I knew that as soon as I totally bought into it, I was going to have to be...I'm a very 100 percent-in person.
3:08 I knew that as soon as I bought into it, I was going to have to uproot all of my developer knowledge and muscle memory, mostly. I was just afraid of slowing down a little bit. I put it off for a really long time. I was really excited about it for a long time, but I just publicly couldn't buy into it just yet because there were just so many things to do, so many things to write.
3:36 It was probably about three years ago, where React Query was starting to get a little more popular. I had a couple of contributors who were like, "We should do this in TypeScript."
3:48 I just told them straight up. I was like, "I don't know TypeScript. I know all about it, but I don't know how to write it. I don't know how to architect with it yet." They were like, "We'll show you. We'll teach you." I was like, "OK, cool. Let's do it."
4:04 I learned from these contributors while they massaged React Query into TypeScript. It was really cool to watch. I learned a lot. I was able to contribute a lot too. That's how I learned. From then on, after that, I was like, "OK. This is it. This is the moment. I'm becoming a TypeScript developer."
4:28 I had to take a few months where I was just OK not shipping as much productive code and just learning. I had to go and migrate. I had to plan my migrations for React Table and Virtual and Nozzle, which is way huge. Nozzle is still not fully migrated. There's still files in there that are just...They're just waiting to be upgraded. It took me a while to just buy into it, but it's fantastic.
Matt: 5:09 With the Nozzle stuff especially, there's...Actually, let's do one layer up from this, which is you obviously work a lot on libraries. You also work a lot on application code as well.
5:22 I interviewed Orta for this as well. He said there's actually three types of TypeScript. There's library TypeScript. There's application TypeScript. Then there's the utils folder for your application TypeScript. How does that sound to you? How do you see those two or those three things being different from each other?
Tanner: 5:47 It probably just has to do with concerns, like cross-cutting concerns versus isolated concerns. A library, who are you serving when you're writing this code is really a good indicator of where it's going to live and some of the characteristics of the TypeScript you're going to write.
6:08 With a library, you're targeting everybody. You want to target as many people as you see fit, that your library is going to help. The possibility domain is massive. You have no idea how people are going to use your code.
6:25 You just have to be ready for everything, which means there's a lot of types, a lot of generics, a lot of preparation, code-wise, to handle all the use cases. Say, a utils folder, it's the same story, but your only customer is just all the other developers in your company or future you.
6:48 There's a little bit more constraint there, where you can make some assumptions about your system and make some assumptions about your data and maybe do some things that you wouldn't normally do with an open source library, like rely really heavily on declaration merging or say, "You know what? It's OK if we cast any here. We know better because we're Nozzle."
7:12 In your application code, honestly, the goal for application code is to have no types. That's really my goal. My application code isn't that way today because there are just edges of your code that, unless you're running a 100 percent type-safe system, there are edges that have to have types.
7:38 Ideally, those edges would only be interface edges for your components, like what do we accept. Utilities, like you said, if you have internal utilities, all of your open source libraries that you consume, and then your component library, whatever.
7:59 I would also say just anything you're doing with network IO or serialization boundaries, so anything you're storing in a database or in a persistent location or transferring across a wire.
8:14 You're probably going to need types for that at some point. Hopefully, that's coming from your back end. I don't know. We use buf.build, so proto and protobuf, gRPC stuff. I don't care what it is, but you're going to need something right. If you don't have anything, maybe you're just using Zod too. Just make sure that stuff's OK coming in from your servers.
8:41 That's how I organize. That's how I think about and organize what needs to be typed and the end goal, the end result. My application code that I'm writing, building new features or building out a new screen or whatever, I shouldn't necessarily need to ever touch types.
Matt: 9:01 That's fascinating.
Tanner: 9:05 If you're refactoring a cross-cutting concern of your library, you might have to write a few. That's the end goal. That's the goal for my libraries too. I'm writing all this insane code so I don't have to write it in my application.
Matt: 9:21 You're taking on the burden of a lot of the advanced types and a lot of the advanced generics. That means that you need fewer any's in your application code. You need fewer interfaces, fewer declarations. You take on that burden.
9:39 The utils folder is a smaller version of that. If you have something that's a cross-cutting concern throughout your entire application, through multiple applications, you can capture that. When you learned all this TypeScript magic stuff, did you notice that you were able to handle those sort of cases easier?
Tanner: 9:59 Honestly, it wasn't a direct realization in the beginning. Unlike what many people should do who are listening to this, they should start with application TypeScript and work their way up and into their understanding of TypeScript.
10:20 I had to do it backwards. I had to jump in at the deep end at the library level, and say, "I need to learn all of the advanced things right now." That was really difficult. I still had to go step by step, but I was doing it in this library setting, which was really difficult.
10:44 It took me a little bit longer to get up and going, where somebody who's writing application code with TypeScript, you can move a little easier because you don't have so much pushback from the type system.
11:02 With a library, everything has to be really, really tight. I didn't realize it at first. It really wasn't until I had gotten most of the scaffolding and the architecture up that I had been taught and learned from all these different libraries. I got it up and running with query and table, and then we wrote the examples.
11:30 Then you start coding in the examples, and you put your application developer hat on. You're in the examples, and then it just clicks and you're like, "Wow, I just made this table, or I just fetched this data, and it all just worked. It came back out, and it's perfect." That's the moment where you're like, "I get it. I understand why."
11:53 Those are the moments where a lot of developers writing application code, they call useQuery with a function that says give me back some asynchronous data. Then that data that comes out on the other end has that type. That's their expectation. They're just like, "Yeah, duh." Thanks, useQuery. Move on. Very few people understand what it takes to make that happen.
Matt: 12:28 It's interesting. You raise a really interesting point there that this inference is becoming something that you just expect that it should work. Five years ago, working in JavaScript, there's no way that you would expect that inference to be even remotely possible, so we live in a cool time.
Tanner: 12:46 Absolutely.
Matt: 12:49 Let's take a look at some code. I want to look at this useBaseQuery function here because there's a whole set of generics. When we did this audience review, we saw maybe up to three generics here, but there's actually five type arguments coming through here.
13:06 I'd love to know what the story is behind these, how have they flow through your whole app or flow through the whole of TanStack Query, and what were the decisions behind it?
Tanner: 13:21 Something that took me a while to realize with TypeScript is that generics flow through your app and all the different functions just based on how you pass generics around. You're going to see TData, TQueryFnData, and TQueryData. You're going to see that everywhere, all over the core of React query or TanStack Query.
13:52 Something to understand is that we didn't register this TQueryFnData generic somewhere global to our system. This is just a name that we chose.
14:05 I will fight anybody who says you need to name your generics as just T or U or V or whatever. Give them names. That's ridiculous. What a terrible bike-shed conversation to have. Name your stuff, but we just consistently named everything.
14:22 You have to make sure that you're passing those generics in the right slots. You'll see, with this query observer function or all the options objects, you have to pass TQueryFnData as the first one to those options objects. If you hover over one of those options objects, it will show you the signature and what it expects, the order of those generics.
14:50 We can talk more about why I hate slotted generics at the end of this conversation, so put a pin in that one. That's how things just flow through. You're just passing generics around into these specific slots. These generics just represent some type that your users have given you. Either that they've supplied directly or a function they've given you is returning or whatever.
15:17 We chose these generics. There's a couple of reasons behind each of these generics. Why we have TQueryFnData and then we have TData and then we have TQueryData? The reason we have three datas here is because there's more than one type of lens or result that you can get into the data you're fetching with React query.
15:47 You can just pass a query function or you can pass a query function with a selector, or there's also infinite queries that you can use as well that all change just a little bit of how the data is actually being returned. You'll notice that some of the defaults here. TQueryFnData = unknown because it defaults to unknown because we have no idea what the user is going to give us.
16:19 If the user doesn't supply a selector or we're not doing anything special, you can see we default TData and TQueryData to the TQueryFnData generic. We're setting defaults here as well. The weird thing about generics is you can specify them from the top down of your architecture, but they can fill in from the bottom up.
16:50 That's something that took a really long time to understand when I was learning about generics. You can see that we're not really inferring anything in these higher types, these higher functions. We're just shuttling generics down into things that we extend or options objects or whatever. Let's dive into QueryOptions here.
Matt: 17:16 I'm looking at...Oh, I see, QueryOptions.
Tanner: 17:19 You now have QueryOptions, and this one's not shuttling a ton, but try and find this TQueryFnData in here. It's going to be in here somewhere. Here we go. We're passing this generic down to a QueryFunction type, and this is the QueryFunction that users are passing in.
17:41 Let's open that up. It says, OK, this is a function that gives you the context that has a queryKey and then returns either a Promise or directly the TQueryFnData. All the way down here in this query function type, users pass in their query function that returns a list of blog posts. That type, that list of blog posts now is going to work its way up.
18:13 If you just -- option...
18:14 [crosstalk]
Matt: 18:15 Let's actually follow it up.
Tanner: 18:17 back up.
Matt: 18:18 We go QueryFunction.
18:19 [crosstalk]
Tanner: 18:19 Everywhere here now, see too TQueryFnData. This behavior right here, it's going to come out of the QueryFunction type. Then it's going to go back down into the QueryBehavior type because we inferred it from our QueryFunction.
18:42 [crosstalk]
Tanner: 18:42 It got pulled up. Then it starts flowing out to all of the edges.
Matt: 18:50 That's fascinating. Looking at that, then, we have QueryFn, which, let's say, it just returned an array of 1, 2, 3 here. Then inside there, expected two arguments, we've got one. What am I missing? Observer, I guess. Let's say, we go const observer = new QueryObserver { }...
Tanner: 19:12 You're going deep here.
Matt: 19:15 Then I'll just say as any for now. Yeah, let's go super deep. Pass in the observer or maybe an observer as any to make it shut up. We have that. Then you're saying the behavior now will be, which is a function.
Tanner: 19:33 Yeah, this is an internal thing.
Matt: 19:34 Let's see. What you're basically saying is then the type that's in QueryFn gets passed into the first...
19:47 [crosstalk]
Tanner: 19:47 write select.
Matt: 19:48 generic.
Tanner: 19:53 Then that data property right there, check out what that data property is. It's unknown, but it shouldn't be. This might be because we're using useBaseQuery.
Matt: 20:01 [laughs] Got you. Whereas if we jump to...Let me take this to...Where is useQuery...?
20:09 [crosstalk]
Tanner: 20:09 UseQuery is coming out of @tanstack/react-query.
Matt: 20:11 There it is. Let's go useQuery. Let's use it here, so useBaseQuery. useBaseQuery is just an internal one. That's just queryKey. Don't need this. We'll get to, I guess, the function here. What you're saying in general is this select then gets properly typed as number array. So nice.
Tanner: 20:37 The inference is coming out of your query function, back down to the select. If you go back to that QueryOptions...
Matt: 20:43 We've got UseQueryOptions, which I guess...UseBaseQueryOptions, QueryObserverOptions, QueryOptions. Wow.
Tanner: 20:57 Come down. Somewhere in here or proxied around is going to be the select behavior. That select behavior might actually be implemented at the React level as well. Actually, if we go to useQuery, the use...
Matt: 21:15 Yeah, it's here. In QueryObserverOptions, we've got a select function.
Tanner: 21:20 There, we're grabbing TQueryData. It's returning TData. That's where you can see. It's taking in an inference from our query function. It's spitting out another generic of TData.
Matt: 21:37 We return data.map and stringify it. You're going to end up with string in this slot here. Extraordinary.
Tanner: 21:45 Even internally, we're calling out to the user with a generic that they supplied and then letting them return a new generic that we will then give back to them. You can chain this as much as you want.
21:59 I chuckled in the beginning because you said there's five generics here. It's like we've only done up to three or whatever. I was literally just in a file this morning that has 15 generics.
Matt: 22:15 15.
Tanner: 22:17 It can get out of hand, depending on what you need it to do.
Matt: 22:21 It's interesting that you've got generics here for stuff that the user may not even use as well. If you just remove the select function, then this just defaults to its default value. In fact, it defaults to the number here, which is really interesting.
Tanner: 22:37 The data that's getting returned is using the very last generic in the chain that we've defined, but that last generic has a default that's defaulting to just what you return from the query function. If you don't supply a select, it's just going to default to what you return in the query function.
Matt: 22:56 Got you. There you go. It's a UseQueryResult with TData, not TQueryFnData.
Tanner: 23:01 But TData is defaulted to TQueryFnData.
Matt: 23:09 If you don't do any inference on it, it defaults to what is. So cool. Because you had a lot of trouble with these generics, right?
23:20 To qualify this a bit more, we've been through a tour now up and down how all of these data shapes interact with each other and how all of the generics are passed through the system because useQueryOptions goes to usebaseQuery options, which has ContextOptions and QueryObserverOptions.
23:45 All of this stuff are passed to individual types here. You had a lot of trouble trying to find a different or better abstraction for this. I'd love to hear more about that.
Tanner: 23:58 Honestly, [indecipherable] wasn't that bad. When we started out, we really only had three generics data. It was the key, the data, and the error. Over time, we gained a few more with select and with paginated stuff, and query has been OK to manage. It's really not that bad. Everywhere you go, you're going to see at most these five generics.
24:24 It was in TanStack Table that I really felt the pain of having too many generics in the very beginning. With TanStack Table, I got upwards into the 8 or 9 to 10 range of generics. Then that's simply just because it's a huge data pipeline library. You're feeding it just raw data of rows.
24:54 Then you are supplying a column definition schema that is materializing an entirely new data set from your selections and your materializations and sorting and filtering in aggregations, things like this. For every step in that data transformation pipeline, they were calling out to the user to say, "How do you want to transform this?"
25:20 Every single time that we do one of those transformations, it's a new set of generics, so they built up a lot. I ran into problems because it just became unwieldy to work with. Passing around that many generics creates a lot of noise in your code, and you start to lose track of indexing, positioning these generics in the right place.
25:47 [indecipherable] it's very easy. Say, you have 10 generics, the last five of them could accept unknown. They just could be anything. If you mix up the order of two of those generics and just swap them, your system's not going to yell at you for that because they extend unknown. It's like, "Yeah, that works."
26:16 Somewhere down through your chain, you're like, "Oh, generic, generic, generic." [laughs] You swap them and then you're down at the edge using your library, trying to write tests for it and write examples. You're like, "What the heck? This says it's unknown, but I know it's not."
26:32 You have to trace this generic. All the passing of your generics, you have to trace them down through your app. It's actually one of the most unmaintainable parts of advanced TypeScript.
26:46 It gets to the point where it's unsafe for you to remember which generics go in which slots. When I encountered that, I tried finding patterns to get away from that. The pattern that really ended up being almost great was generic bags and generic maps, which is essentially where you make a generic that is essentially just an object that holds a bunch of other generics.
27:17 It's similar where if you get more than two or three arguments in a function, what do you do? You turn it into an options bag. It's the same thing, but for TypeScript.
27:27 The problem is that a lot of people using JavaScript understand what it means to pass an object as a function argument, but very few people understand what it means to pass and work with generics in one single generic slot.
27:50 Also, inference starts to break down, too. TypeScript just isn't built to...Remember how I was saying you pass your data down and then it grabs your inference here, and it sucks it all the way back up and makes it flow out to all the edges?
28:09 When you use start using generic maps, it stops doing that automatically. You can still do it, but it stops doing that automatically. You have to start using some interesting utilities. You have to tell TypeScript not to infer certain things. No infer, very popular utility. You have to start writing your own higher-order types to set defaults on generics that are set to unknown.
28:44 It's like when you say object.assign and you're saying, "Here's the default, here's the ones I have," merge in the empty gaps. TypeScript does that automatically with the slotted generics, but you have to write your own utility to do that and use it everywhere. You end up having utilities like merge generics.
29:06 You give it the default generics and then you give it the generics that you have from the user, or maybe you have multiple layers of that and it's like, "OK, merge, merge, merge, merge merge." It gets expensive.
29:19 People just don't know how to do that. I almost shipped it. I was this close. I literally just responded to a tweet that was like, "What happened to that?"
29:31 I did my talk on it at the React Summit. I was like, "This is so cool." Then I ended up pulling out at the last second because I shipped it as a beta, and a bunch of people were like, "What is this? What is a generic map? How does this even work?" I was just like," Never mind. Never mind."
29:51 I went back to 10 generics. After that, I started swinging back the other way a little bit. I was like, "If I can't do this with just generic slots, I'm going to do this with declaration merging." I got my 10 generics in table down to 6 or 7. The ones that I took out are just now interfaces that I and users can declaration-merge.
30:24 Some people stay away from that. It's like a poor man's generic a little bit. I do some really weird stuff with it in declaration merging. In fact, I should show you. Do you want to see something I'm doing with it?
Matt: 30:43 Go on. Let's have a look.
Tanner: 30:48 One thing that is difficult when writing...This is in the router package that I'm working on. Let me show the example first. Let's just do the basic example. This is what writing code for the router looks like. Essentially, you have these links. Everybody's used to writing links. Let's...
Matt: 31:19 Could you bump up the zoom a little bit?
Tanner: 31:23 Everybody's used to writing links, but how does your system know that to="/posts" is going to be a real thing? This will autocomplete. If you type slash or just type open the quotes, it'll autocomplete all of your posts. How the heck is it doing that?
31:43 There's nothing else here that's giving it information. The only place it's coming from is here. That's coming from the actual library itself. At what point does it find out about my routes that I have created here? It knows about this route right here, this path posts. How does the link component know that?
32:12 This is how it knows that. You define all your stuff. You create your router. You come down here. After you create your router, you register your router for type safety. You declare the module, tanstack/react-router. You grab the interface RegisterRouter and you put type of router on the router.
Matt: 32:34 This is fascinating. To put this in context with the Total TypeScript stuff, I've got a whole module in advanced patterns, which is about globals. What you're doing here is you're saying, "OK, we create a router," I should say router, shouldn't I?
32:58 We create a router and then we then use the type of that router to place it in a global interface. Could you go back to where that code was? Let's see.
Tanner: 33:09 Right here.
Matt: 33:14 It takes a hot second to update on my end. There is a typeof there that does all of the work for you. RegisterRouter, router typeof router. Fascinating. Then things that you then import from the library will just work, and we have this beautiful inference.
Tanner: 33:32 The way [indecipherable] under the hood, we have an interface called RegisterRouter that is empty.
Matt: 33:39 Sorry, Tanner, the connection has dropped right off a cliff. Could we stop the screenshare if that's OK?
Tanner: 33:51 Long story short, there's an interface inside of the router package that is empty. There's nothing in it. During the compile time runtime of TypeScript, on the first pass, there's nothing there when link tries to grab its types. After you declaration-merge, TypeScript is always running a little bit or doing multiple passes.
34:20 You declaration-merge and it goes back through. Now there is a router key on that interface. We have a conditional type that says, "Give me the registered router." If there's no registered router available on the interface, it returns the default one. If there is a registered router, it returns the one that got you registered.
34:43 The biggest constraint here is that you're not going to have multiple routers in your system. If you do, it's fine [indecipherable] you work around it. That's one of the tricks that I did to get around having to pass so many generics.
35:03 If there are generics that stay the same mostly throughout your system, you can cheat and move those into a declaration merge utility. If there's generics that literally change at every single call site, those need to be normal generics. There's just no way around it.
Matt: 35:22 Let's go back to the generics on useQuery, because there's an interesting part of that. That global stuff is so fascinating to me. Because we're a little short on time, I want to jump back into this code here. I want to talk specifically about the overloads that you've chosen because useQuery is overloaded quite a few times.
35:50 You've got a useQuery here that takes this one, then a useQuery here, a useQuery here, a use useQuery here. What? We have up to five overloads, six overloads, seven overloads. Is that eight? Eight overloads, and then the function implementation. Eight total overloads.
36:08 I'd love to hear more about the way you think about overloads, whether they're good or bad, or what's your...?
Tanner: 36:16 I hate them.
Matt: 36:17 Hate them.
Tanner: 36:22 The overloads and overloads and overloads to me is usually a sign of migrating the patterns of JavaScript into TypeScript, where in JavaScript, you don't need to worry about your function signatures as much because you're not inferring. You're not using TypeScript at all so who cares.
36:47 You say, "Sometimes we get a string. Sometimes we get an object. I know the difference between that. It's just an if statement." TypeScript is a little harder. What you see here are all the different overloads to handle the different function signatures we've had over the years for React Query.
37:04 At the end of the [indecipherable] because you have to maintain all of those different overloads. Then in your implementation, you have to handle it still with...you have to handle these five different ways to call your function. I usually -- I will never say always -- but I will generally say that I don't use overloads really ever.
37:29 Unless I'm trying to type a function that already exists that works this way, I really stay away from it. In the future, in the next version of TanStack Query, all of the overloads are going away. There's only going to be one. You're going to pass an object with these.
Matt: 37:53 Interesting. The reason for that is you don't want to bother maintaining a lot of these. What's the reason that you don't like them? The feeling that you get when you're trying to represent all of these different call signatures...You can take a query key as the first parameter. You can take a query function, is it, or like an options bag?
Tanner: 38:19 You can take an options bag, yeah.
Matt: 38:19 What's the reason you hate them?
Tanner: 38:21 It's just a lot of cognitive overhead, for both you as a maintainer and for the users, to say, "Hey, here's six different ways you can call this function." They're going to say, "Well, why?"
Matt: 38:36 Yeah, I see.
Tanner: 38:38 In my opinion, if you need different ways to call a function, you should just make a new version of that function that accepts that specific style or that signature. If there's no benefits or pros and cons to that signature, then why do you even have it, other than you're just trying to port old JavaScript code into TypeScript?
38:59 To me, in my opinion, I think that it'd be easy to say, "Well, usually, query keys are just a single string. Why don't we allow passing a single string?" That's going to add complexity to your library code too, like your implementation code. Now, everywhere, you have to worry if your query key is a string or an array. You don't know for sure.
39:24 Putting constraints on your system is a good thing, as long as those constraints are composable and friendly. What we found was that the worst case scenario is that you want to configure everything. When you want to configure everything, an object with options is the syntax. It is the signature that we want.
39:44 We're always returning the same thing. That's the other interesting thing. All of these overloads are returning basically the exact same thing, but just from different styles of options objects. That's another indicator that they need to go away.
Matt: 40:00 I agree.
Tanner: 40:00 If you have function overloads that are returning different things, I would play devil's advocate and say, "Why are they just not different functions or variations?" One can consume the other, if you want, and return something different. That's where I stand.
40:20 That leads itself a little bit into how do you handle dynamic options and things? I do that with generics and with conditional types, conditional return types. You can still return slightly different things based on your options. You just have to use generics.
40:41 I think people are uncomfortable with generics. They're like, "Overloads are a lot easier. I can just pattern match on this style of syntax." It's a cop-out, a little bit, from just using a generic and saying, "If this generic that they gave me is this, return this. Otherwise, return this."
Matt: 41:03 It's interesting. There's three points there, which is avoid polymorphic APIs, basically. Try to make your library less polymorphic and you will need function overloads less. There's less complexity there.
41:17 Then it's if your function overloads are all returning the same thing, that's a code smell. There should be something you can do with generics in there to make it a little bit easier.
41:30 There are use cases, I imagine, for...Because I think of function overloads as really nice for the utilities folder. It's actually, I would say, easier to maintain a function overload than it is to maintain a conditional type, probably, for someone who's like...
41:48 [crosstalk]
Matt: 41:48 If you're the only TypeScript dev at the company. For most people, it reads pretty easy.
Tanner: 41:54 Pattern matching is much easier to grasp than conditional types.
Matt: 41:59 For sure. I'm looking through my list. I wonder if there's one more thing we can talk about. I'll tell you what. Let's finish with a fun one.
42:13 Let's talk about the future of TypeScript. You now maintain a lot of libraries that depend on TypeScript, that need TypeScript to work in certain ways in order to make it happen. What features do you wish were in TypeScript that aren't?
Tanner: 42:30 One of them I just talked about was the generic map stuff. I wish there was a way where you could pass as many generics as you want, in and out of function signatures, without relying on them being in an index slot. I'd like a built-in way.
42:49 There's two ways, I believe, you could solve that. I think there have been PRs to do both of them. One of them is actually having first-class support for passing objects with generics in them and just having the inference just work great.
43:03 The other one is using named generic slots, where instead of passing them in an index base, it's like named arguments in other languages, where you have to say, "This is the generic name equals the value," or it would be a colon or something like that. You would name your generics.
43:26 There's also things that we didn't talk about. One of them was when you manually cast generics, like when you pass a generic, as soon as you start passing one generic, all of the inference goes away for the others.
Matt: 43:42 Let's actually look at a bit of code for that because that's really interesting too. Some libraries actually encourage you to manually pass in a generic yourself. If we say like const...
Tanner: 43:56 In fact, do this. For useQuery, when you call useQuery and you cast the data in the first generic slot, it will no longer infer your error type that you're doing. Actually, select won't work. Yeah, queryFn return hello:true. In your selector, just return a string, basically.
Matt: 44:30 Return, blah, blah, blah. Now, basically, this isn't getting inferred. We've got hello:true there. That's now being inferred as TQueryFnData. Whereas if we just remove this, then this is actually just inferred as string, inferred properly.
Tanner: 44:51 What if you only wanted to type the error generic? It's easy to do this and get around it a little bit. Say you wanted to keep inferring the data, but only cast the type of error that you're getting. You can't do that without first defining the TQueryFnData. There are ways around this.
45:12 Actually, we're not guarding against inference coming up from error handlers. If you do onError, I believe, as one of these options -- just do onError -- you'll get the error right inside of that first callback. Cast that error or just type it. Just do err:something. Yeah, wow=true.
45:40 Go back to your useQuery, and you'll see that it's pulling it back up. The way that I've been able to get around this is if there are generic slots that I want people to be able to set manually what that is, I provide some type of option that allows them to pass that type.
46:07 I've gone even as far in TanStack Table of having a types array or a types object where you can say types, and then an object, and then pass Error. It's an upper-case Error. It would look like this. It would be like this is the error generic. You pass [indecipherable] or an object as whatever you want.
46:35 Then you would infer from that, essentially. It's like an optional inference path. It's like sidestepping the generic casting, but doing it inside of the options object. You're still casting just one of the generics, but you're doing it through inference instead of through casting. I believe that's safer, to be honest.
Matt: 47:00 What we did in XState is we had a schema property that you could pass context and events to, and then we did this pattern here. It's pretty ugly. It's, as you say, working around the fact that if you pass something to this, you have to pass everything, which is gross.
Tanner: 47:20 A lot of situations where you run into this, you can usually supply something like a validator or something that uses odd schema and parse this random thing, and then whatever you return can be that type. In this situation for error, there's no way to know what that error is other than just what you say it is, because JavaScript is weird and you can throw anything you want.
47:49 There's no way to infer the error type that's going to be thrown from your query function. There's just no way to do that. There are circumstances where you need to provide your own schema and hints to TypeScript.
48:04 That's one way to do it. That's a big gripe of mine, though, is the whole generic management. Scaling generics and managing generics as a library developer is just terrible. I believe that if they were better, people might use generics more not just in their libraries, but in their apps. Part of that is what makes it scary.
Matt: 48:23 For sure. I can definitely see that. The fact that you have to rely on just a placement in an index is just gross.
Tanner: 48:35 The other one, really quick, that is being worked on right now, Mateusz...What's his last name? Mateusz.
Matt: 48:41 Mateusz.
Tanner: 48:42 Brzezinski.
Matt: 48:42 Mateusz Brzezinski. I would call him up the next day.
Tanner: 48:46 He's working on it right now. It's the satisfies operator, but up inside of generics or in the arguments layer, where instead of saying, "Hey, I satisfy this thing, but take me as I am," you put that constraint into the function itself so that users don't have to write as satisfies or as const.
49:10 They can just pass whatever they want and then you put that constraint in your function and say, "This thing satisfies this constraint, but I want to infer it as what it is, as const, without type narrowing it."
Matt: 49:30 I see. Is that the const modifier thing?
Tanner: 49:34 Possibly. I haven't looked at it exactly.
Matt: 49:38 Possibly.
Tanner: 49:40 There's type utilities to do this where you can say, "Don't narrow whatever I'm getting," but you have to set it up very specifically, your whole system, to do that. I have to do that in a lot of places with the router that I built. All the route definitions are recursive constraint identity functions.
50:03 Every single layer, you return a constraint identity function, which then lets you call another constraint identity function, and chain, and chain, and chain. It gets really complex. You could get around all that, though. TypeScript just had these constraint identity features built in. Those are the two things that I wish.
Matt: 50:29 I'm going to have to look at that for the constraint identity function module that's in the third module of whatever. I really need to dive into the code...
50:41 [crosstalk]
Tanner: 50:41 The other one, too, that's a little more abstract would just be doing more of identity typing, primitive typing. There's an experimental language out there that I can't remember what it's called. I just recently saw it, where it's like TypeScript but it can understand so much more about what's possible with...It treats everything as a generic just automatically. Every...
Matt: 51:06 Is it TSplus or...
51:11 [crosstalk]
Tanner: 51:11 I'll have to find it and we can put it in the show notes after. Essentially, it's written so that every single argument and every single structure you pass around in the system is just treated as a generic that's either getting extended, modified, or shaken down into some other type.
Matt: 51:33 It understands a lot more deeply about what's going on with those things.
Tanner: 51:41 It understands so much more about your usage of the code. It's understanding as much as possible from your use, from your environment, into the TypeScript or whatever compiler it is. That's a little more abstract and out there. That's more of a pipe dream maybe.
Matt: 52:00 A world to live in, though. Thank you so much for your time. This has been absolutely amazing to see some of the decisions that you made in React Query and TanStack Query and understanding especially like the pain of managing all of those generics, your hate for your function overloads, and all of the cool stuff that you've introduced us to.
52:25 Thank you so much, Tanner. [indecipherable] burger.
Tanner: 52:27 Absolutely. It's my pleasure. I just wanted to hang out with you for an hour. That's all. There you go.
52:32 [laughter]
Matt: 52:32 Mission accomplished.