Advanced TypeScript: Let’s Learn Generics!
with Matt Pocock
If you've never used typescript generics, it can be pretty intimidating. In this episode, Matt Pocock will teach us this advanced TypeScript concept.
Resources & Links
Captions provided by White Coat Captioning (https://whitecoatcaptioning.com/). Communication Access Realtime Translation (CART) is provided in order to facilitate communication accessibility and may not be a totally verbatim record of the proceedings.
JASON: Hello, everyone. And welcome to another episode of Learn with Jason. Today on the show we've got Matt Pocock. How are you doing?
MATT: Doing well.
JASON: I think we have proven that you sound 20 IQ points smarter whenever you have a British accent.
MATT: I think you sound more charismatic as American. That's my take.
JASON: Yeah. No, the thing that I've noticed, there really is a musician to engineering pipeline. Because that -- I think it's a similar problem solving space, right? Like you -- you're looking at patterns, you're looking at ways to combine old patterns in new and novel constructions. And that's what makes things interesting and exciting. And so, what I found, I've made this joke before. When I was a musician, when the band broke up, I learned that the only thing about being a musician that I wasn't good at was making music. All the other parts, the booking and like all these things that were business skills, right? How to go on tour. And also the construction parts of music. Like understanding patterns and all those other things, that all felt good to me. So, when I moved into a Dev, oh, man, like 95% of these skills crossed over. It's really interesting to see how that works.
JASON: Yeah. And we're seeing like a couple really good comments here. Music is the original programming language. That's true. Because if you think about it, when you write sheet music, you are providing a set of instructions to a future -- I mean, to a machine. The machine was hand-operated. Like a piano or a violin. A set of machine instructions. That's fascinating. Math is the OG language. Yeah, you're probably right. But really, this stuff is so fascinating to me because it really is -- it all just comes down to patterns, right? How do you communicate somebody else what you wanted -- what you intended? And so --
MATT: To me, the other thing too, the problem solving element of teaching was something I just got addicted to. Especially with like a singing teaching and voice teaching. Someone comes to you with like a specific problem. Like I can't hit this note. And it's like a mechanical thing. You know? Often it's an emotional thing too. Like you have a sense of the person feels like they can't get there. And so, their body tenses up and all this sort of thing and you get this tension. It's correcting that mechanically. So, interesting and there's so much in-depth knowledge out there. And so, coming to Dev, it felt like, oh, this is -- we have this very complex system that we're building. And it actually felt much easier than voice teaching, weirdly. You can't componentize the voice.
JASON: Most stayed away from the advanced topics. We were building the foundational knowledge. If you are watching this today, if you have never used TypeScript before, you want to watch the video on demand. What are types and how you set up a TypeScript repo with the TS CLI and stuff like that? So, Matt. Why would someone need this, like, really advanced TypeScript?
JASON: Yeah, yeah. And so if I kind of grossly oversimplify what you just said, when we're talking about libraries, libraries are dealing with a lot of unknown inputs. And so, TypeScript gives you that really advanced capability to do the inference and everything you need so you're using typing as you're using the code. That's a really good use case for advanced TypeScript. If you're writing a library yourself and you know what the inputs are and you're not taking a bunch of arbitrary user line code, you may not ever need this. Is that a fair assumption?
MATT: If you're building a team, they're force multipliers for the team. Doesn't matter if it's an application or -- if you're in charge of the applications, then you can really enable a lot of power to people.
JASON: Okay. So, then a revision, then. If I can kind of walk this back a little bit. Is that where these generics and other advanced TypeScript concepts really start to shine is when you are building code for writing code. Right? Like so if you're building an application and you're gonna ship that and that's what runs in the browser, you probably can figure out enough about how your code works to not need any of these really advanced things. And personally, I'm a big fan of like if you can refactor something to be just a standard type, you probably should, right? Like don't get clever if you don't need to get clever. But as you start writing code that's a layer of abstraction to empower other Devs to build more things. You've found a common pattern and people are gonna wrap all their code with this thing that adds some thing. Then suddenly you have this deeper need to like accommodate for people are gonna do arbitrary stuff. And you want to support that without making them do a bunch of extra lifting.
MATT: Exactly. That's every Dev's code, write code that empowers other Devs. That's why people get excited about open source work, and they're enabling people to work faster. That's what this allows you to do.
JASON: Absolutely. So, I've said the word "Generic" a couple times, and I'm going to do a little confession here. I don't know what that is. In the highest -- like lowest level of fidelity, what is a generic in TypeScript?
MATT: I can't wait to explain this with code.
JASON: Yeah, I'm actually wondering if we should do that. Okay. But first, we've got one -- I don't even know if this is a spicy question. But it's a good -- it's a good question here. So, big hairy dude, nod for the wonderful username, asks, are generics a work around for the fact that TypeScript has locked you into types?
MATT: No, I don't think so. I think -- I think you might mean has locked you into using types. Forced you make sure everything is correct. I think that's my interpretation of the question. I think.
JASON: Yeah. Because like my gut feel on that is no because the work around is to just mark everything as "Any." Like if you really -- if I have a library and I'm like, I don't want to figure out how to type this thing because you're literally passing arbitrary code, then I would just make an argument called code, and make it any, and you send me whatever you want. That's the work around. Generics are almost the solution for needing that work around.
JASON: Gotcha. I'm with you, I'm feeling it. Okay. So, knowing that the rest of my questions are absolutely going to be easier to explain in code, why don't we just hop into code and make that happen? So, I'm gonna take us over to -- let me get the right things on screen here so that I'm not putting us into the infinite loop. And then I'm gonna take us into pair programming view. And so, first, this episode, like every episode, is being live captioned. We have Amanda here with us today from White Coat Captioning. Taking down all of these words. Thank you very much. If you want to watch that, that is on the home page of the site, learnwithjason.dev. And we are also talking to Matt Pocock on Twitter @-- what did I miss? I added too many names.
MATT: Probably reflected to that, anyway.
JASON: I'll drop this in here. Everybody give Matt a follow. And we're talking about TypeScript which is something you have been working on a lot. You have this Total TypeScript site, that is absolutely gorgeous, by the way. Look at these illustrations. Really there's some good stuff going on in here with --
MATT: I did not do this. My favorite bit is this error message on fire. I said the illustrator, I want the error message to look like it's coming from the depths of hell. And she did an amazing job.
JASON: It's so good. It's so good. No, this is wonderful. So, I love it. I love it. This is -- so, this is really fun. Everybody go check this out. Make sure you get on the -- there's an email list on here, right? And that has a bunch of free resources on it. So, make sure you get there. And with that, I have absolutely no idea what to do next. You had me -- let's see. Let me clear this out. You had me download this repo.
JASON: And so, I've got that. And that's over here.
MATT: So, yeah. This is on -- maybe we want to link this as well. This is the Total TypeScript, what is it? TypeScript Generics tutorial.
JASON: Let me find that. It is here. Go to GitHub.com/total-TypeScript. And then I need this name here.
MATT: Yep. You got it.
JASON: There it is. Okay. Let me throw this to everybody. There you go. Got a lively discussion about generics happening. Thank you all for getting in there. I'm looking to see if there are questions. Doesn't look like there are. We will carry on ahead. We have this repo. This repo is part of the Total TypeScript thing. But we are getting into generics today, which is the advanced TypeScript that I want to learn. And I've cloned it. I have it here. So, what should I do first? Like do you want to do a little tour of the repo?
MATT: Yeah. Sounds good. The repo is simple. If you go into the source directory, what you'll see is a bunch of different problems and solutions. And all you've got to do to complete this repository, to complete these exercises is go through them one-by-one, look at the problem first and then see if you can solve the problem that's in there. Usually that problem is, there's a type error in there or there's something wrong with it. A lot of them are not kind of explained perfectly. Like I haven't gone through and added comments through all of these. This is still a semi-work in progress still. But what we should see it see if you can solve the problem and understand it. And if when you are finished, have a look at the solution or solutions if there are multiple. The solutions are usually telling you didn't ways that you could have solved the problem. And what you can do is when you finish this, you can go in chat as well. We can do this together at the same time. And you can ping me on my Discord as well. There's a bunch of people in my Discord going in this. I should drop a link in the chat.
JASON: Is that in here somewhere?
MATT: It's in the linked there. It's relatively new. I will drop a link in the chat.
MATT: But that's the fundamental stuff to do with the repo. What we should probably do is do an npm install. That's the next step.
JASON: Npm install. Okay. So, there we go. Here's the Discord. I'll go ahead and accept that invite now. I can't, because I don't have Discord open. I'll do that later. But yeah. So, oh, that installed faster than I expected. I thought we were gonna have to kill some time.
MATT: There's not much in here. What we have to do now is run npm exercise-01. Very nice. So, what this is gonna do, then, is it's gonna do -- oh, we finished the exercise. That's unexpected. If you go into exercise 01, what you'll see is that there's a challenge. And there's a comment at the top.
JASON: Okay. So, we are -- all right. Let's take a look here. How do we refactor these to reduce the duplication in the data declaration? So, we've got user data that has data. We have post-data that has data. Comment. All right.
MATT: And then this stuff at the bottom is basically a bunch of tests. It's testing that the types above it are what they say they are. So, if you comment on the line 24, for instance, on that comment and just remove that, then we're going get a type error down the bottom.
JASON: Oh, and it's running as we go. Oh, perfect. Let me pull this over here. And then I'm gonna close this up. And then -- we get an error down here, nice. Okay. Great.
MATT: So, here, our challenge is, we've got these three objects and they all specify a data attribute. It's kind of what I was describing before, actually. And so, there's an opportunity to dry up this code. We can take these post data, comment data, and the other data, what is called user data, and we can -- there is a way using TypeScript that we can dry them up. So, with your --
MATT: Extensive knowledge of Generics, Jason, take us through how we do that.
JASON: Here's my guess. If I wanted to pseudocode this, I wanted an interface for like a data wrapper. And that will take one field of data. And then that needs to accept options. It will take a user. It will take post. Or it will take comment, right? And so, that would be my -- my gut. And then you're making it sound like this should be generic. So, the data should take a generic. Okay. So, if I do -- let me see what I can figure out. Again, I haven't actually written one of these before. So, I'm going to say interface Data. And then generic is the T, that's right?
MATT: That's correct.
JASON: I put a T in. And then in here, my new user data would be like export interface UserData extends? Implements? No, what is it? Something. Data, right? But then it's still gonna be -- okay. I've -- I now need to know more about how this works to start. Because if I was gonna do it this way, I'm already -- like all I'm doing is saying, oh, this should be data. And that wouldn't work. So, I would instead want it to be interface user, which would be these. And now I don't know what I'm doing.
MATT: This is fascinating.
JASON: I need to get this into here.
MATT: What I'm seeing, you're approaching it from an object-oriented thing, it needs to extend from this. When I think you should be thinking it in a more functional way. Why don't I drive. You are super-close. You can see my cursor, right?
JASON: Ah, okay. All right. So, we've just touched on two things that have always confused me. First, is the inclusion of these angle brackets. And the second is the letter T. So, T, we've decided, is generic. It's a place holder for whatever type comes in. Right? And angle brackets are the equivalent of a function call in a type, is that correct?
MATT: Exactly right. They're the equivalent.
MATT: If we could write them with parentheses, weld. We would write dataWrapper T. Like this.
MATT: This is the syntax that TypeScript has is chosen. We can call it anything. We can call it Tdata, TJason, we can call it thing. T is just a convention. We can add as many as we want. T1, T2, T3. It's just function arguments.
JASON: Got it. Okay. Well, that's I would say the first major lightbulb moment of the day is demystifying what the heck these are in TypeScript. And to all of you who already know, congratulations. To everybody who just learned along with me, you're one of today's lucky 10,000!
MATT: So, here then --
JASON: And if you don't know that one --
MATT: I do not know that one.
JASON: It's a comic from XKCD about learning things. Very good. Let's --
MATT: I can't just read it live on the stream. That's not good entertainment.
JASON: No, it will be in the show notes.
MATT: We've now got the function that turns in the T data and returns a wrapper with T data here. We can delete this dataWrapper because it's doing the same thing, but on the type world. And now we have an issue. Which is our user -- we've got our user data down there. And we need to somehow like call this function to return and you type.
MATT: So, the way that we do that is we would say, let's say we have type, whatever. And I find myself doing this a lot in TypeScript by the way, just creating these sort of dummy variables. And I'll show you why in a second. We can say now dataWrapper. And we can call it with whatever actually whatever we want. Let's just call it with a string. A string like this. And now if you, Jason, if you hover over this whatever thing, here. You'll see, it's typed at dataWrapper string. Now, let's actually create a variable called const whatever. And we're gonna give it a type of Whatever. Now, inside here, if you hover over that error, what's that telling you there?
JASON: Property data is missing. But required. Okay. So, then if I come in here and I add data. And I want to do one of these. Now it's mad at me. Because it wants a string.
JASON: Now we've got some useful stuff happening.
MATT: Yep, definitely. And actually --
JASON: I think I can take over from here. I think it just clicked for me. What I want is I want my type of user data to be a dataWrapper of user. And then I want this here. Because now what I get is if I do a user -- user data. And then I have my data. It autocompletes with my stuff.
MATT: There you go.
JASON: Wait. But it wants it to be a string, doesn't it? There we go. Okay. So, is that how you would have done it?
MATT: Well, let's check the solutions.
JASON: Okay. Great. Let me comment this out. So, it's not yelling at me. And then we'll open this up. Let's look at the solution. Blow this out a bit. Okay. We have the dataContainer, that's what we did, user data, comment data. Oh! Oh! I see! You abstracted it even further.
MATT: I didn't --
JASON: You de-abstracted it.
MATT: Yeah, I didn't declare the other interface. I just went in and put the object primitive in there.
JASON: Got it. I didn't know you could do that. That's really handy. We have a second solution here. That an alternative approach?
MATT: That's right. Alternative approach.
MATT: And that one I think is declared data contain we are a type instead of an interface.
JASON: Oh, with a type instead of an interface. And so, in this instance, does it matter? What's the difference between a type and an interface?
MATT: Types can represent anything. Whereas interfaces can only represent objects and functions in some cases. That's the thing I get asked most of all. Should you use a type or an interface? And the answer is, you can use whatever you like. Basically. As long as you're consistent, it doesn't really matter.
JASON: Okay. Okay. Cool.
MATT: We are finished.
JASON: We did it. We did a TypeScript. So, yeah. This is -- this is really, really good. Oh, my goodness. My middle name is Danger read my book, Untethered. I forgot that book existed. Yeah. That's a blast from the past. I have not done that -- let's see... in like 2014 I lived out of a suitcase for a couple years and I wrote a book about some of the stuff that I did. Like how I made that work to get out there. That's what that book is. It's self-published on Amazon. I think a lot of it might be outdated. and it's definitely gonna be a surprise what this stream is about if you came looking for Untethered content.
MATT: I should say my wife is writing a book as well on -- identification.
JASON: Oh, that's gonna be a lot of interest to my partner. All right. Let's do another one. Should we just do these in order? Or what you want to highlight. I don't think we have time for all of them today.
MATT: Number 2 is a banger. Let's do number 2.
JASON: Number 2 is a bang -- all right. So, this one doesn't have a comment on it. So, let's just look at what's going on. So, this one -- let me do two. It's mad. It says type false does not satisfy the constraint: True. Okay. That makes sense. And let's go and look. So, return what I pass in, unknown. It returns it.
JASON: Why does that happen?
MATT: Yeah. You think that -- because really what we want is if you hover over, on Line 7, hover over 1 there. You would expect this to be of type 1. That's what you've put in there, right? You've put in the literal number 1, and you would expect it to just be returned there. But actually, it's typed as unknown. And the same is true for Matt down below.
JASON: Right. Oh, and I see, down here we're saying type of 1, 1, type of Matt, Matt. We're not getting that. That's where the false is coming from. Okay. Now I understood how these tests work. So, I can then up here we need to figure out how to do --
MATT: Don't look at chat. Someone's already got it right? Chat.
JASON: All right. Chat, shield my eyes. All right. I'm gonna do an interface. And I want it to be... I mean, should it -- is it... is it just like... wouldn't that... nope. That's not right. How do you return something from TypeScript? Because what I'm thinking is if we did it at a function, right? It would be you get a type. You literally just want to return type.
MATT: Right. One way you can express that is with a type itself. You can say type Fn. You go from there. I'll let you drive.
JASON: Yeah. Okay. So, I do a type of Fn. And then I need to pass it in. A T type generic. And then I need it to return. And what I don't know is what's the -- like you can't just do one of these, right? So, what's the syntax here? Is it -- that's not right either.
MATT: I'll give you a clue. Which is you're entirely -- well, you're on the right track conceptually, but syntactically you're in the wrong place.
JASON: Yes. Okay. So, if my type is like this, and it needs to return something. The thing that it returns is one of these, right? But I still don't know... wait, I know I can't just do that.
MATT: I'll give you a clue, it actually needs to be --
JASON: This is where my syntax falls apart.
MATT: There's a few ways that you can express this. But the way that chat is pointing to is -- it actually needs to be sort of inside this function declaration here. So, it's -- we're actually going to inline the generic here. And the way --
JASON: Okay. So, I need to do something like... this and then this?
MATT: You're like --
JASON: And like that.
MATT: You're so close. I'm going to leave this so chat tortures themselves for a second.
JASON: Come on, that works. What are you doing?
MATT: It's really close. Let me show you. So, you were dead close with this, but syntactically, what we have to do is just move this to the other side of the equals. So, this now declares it as a generic function instead of a generic type.
JASON: Got it.
MATT: So, what's going on here, and actually, you were thinking, how do I say -- how do I return this? Well, in TypeScript, you can declare function return types using this colon here. If I say I want it to return a string, I would do this. But actually, let's say we go to TItem here. And TItem here too. And actually here we're going to return TItem.
JASON: Right. So, that's a lot of repetition here. Does that dry out at all? Like because I guess the thing that I notice here is that we've got like, okay, so, we're passing an argument to our type. We're passing an argument of the type to our function. And we're returning our function. So, there's not a way for this to like infer where if we were to drop this entirely, it would still work? Clearly not.
MATT: Yeah, if you do that, it looks for a type in the above scope of the TItem. With the name of TItem.
JASON: Okay. Gotcha.
MATT: This is a type declaration. And this is available for the rest of the thing. We have to add it here. Because we have to then assign that to an argument.
MATT: So, that's the reason that's there. We can actually drop this. Really cool thing about TypeScript is that it knows that if you hover over this T just here, you'll see that it's actually typed as TItem. It knows this is the thing we're inferring from. So, this is the thing that we pass in.
JASON: It can do inference. But you have to make sure that it's got the right places to work. So, this is the type declaration and this is saying where the function uses that type declaration. But if we're just returning it directly, then TypeScript is smart enough to know -- got it. I understand.
MATT: So, that's the type declaration. And that is the type assignation.
JASON: Now if I save this, our test should pass.
JASON: It did. Great! And so, you've got a couple options in here. So, this is like a hard --
MATT: Well, this option you'll notice that there's no T happening here. This is using a technique called function overloads. And function overloads allow you to actually make the tests pass here. Because what we're saying is on Line 3, got return what I pass in. If we pass in Matt, return Matt. Up here, if we're saying if we pass in 1, return 1. And then down here, we say, we actually implement the function and we say, return what I pass in, which is unknown. And we return the T. And so, this does make our test pass.
JASON: So, this one feels a little bit like when you write a function like is odd. And then you start doing like if...
JASON: Yeah, like one of these.
MATT: That's such a great way of putting it. Yeah. That meme from whomever posted it. Right.
JASON: Right. And you find yourself writing all of these like. Ope. Better do another one. Ope, better do another one.
MATT: The thing I remember, it was such bait for the mansplainers. Did you use the module? So good.
JASON: What was that? Cat Maddox? Such a good post. I'm not going to be able to find that actual post. But I am gonna kind her Twitter because it's amazing. It is full of -- full of good information. Cain. Oh, Cain now. Okay. Yeah, Cain is freaking hilarious. So, all right. So, and then what we've got here... is the generic. So, I'm gonna undo my work in here so that we don't get... all right. Oh, here's our -- here's our generic. Great. And that's what we ended up doing. So --
MATT: Yeah. There's one thing to note in here before we continue too. Which is that TypeScript give use like really good -- oh, trash is raiding. What's up, trash?
JASON: What's up, trash. Thank you for the raid. Welcome, everyone.
MATT: He considered himself an acolyte wizard, going through the challenges. Like a Hogwarts first year.
JASON: Gotcha, gotcha.-
MATT: He's passed his AWLs. He's smashing it. Roll through this stuff.
JASON: Great, great, great.
MATT: So, the thing to look at here is that if you hover over -- sorry, let me get back to the file of mine. Oh, we're in the solution 2, aren't we?
JASON: Do you want me to move back?
MATT: No, if you hover here. Return what I pass in. You'll see that you get the generic actually get locked in there. Which is super-duper useful.
MATT: And create another one of this, const obj, returnWhatIPassedIn. And 32 -- I don't know why GitHub Copilot thought was 32. And then you pass in, so, TypeScript is really good at giving annotations to tell you what generic has been locked in at what time.
JASON: Sorry, the chat is just giving each other crap and it's making me laugh. And this is the stuff that I think is really useful here. Because this is the piece that's magical, right? Because later, when we go to use this, it autocompletes for me based on what we're using, right? And so, that -- that's the part where... like early on when people were telling me about why I should care about TypeScript and they would talk about, oh, you want like compile errors early and, you know, you would want to have that type safety. I don't care. I don't care. I don't care. I'll fix that error. When the bug happens, I'll fix it. I understand academically, eat your vegetables, write your tests, declare your types, all those things. But also, I'm under deadlines and I don't want to do it. Then someone showed me autocomplete. And now I care about TypeScript. The idea that if I write these types, I don't have to go look at the code declaration to use it. Absolute game changer. That's the part where I started including first the JSDocs syntax, defining my params to get type completion. And then I saw, that's not going to bed a flexible as you need, I took it one step forward. I started writing actual types. If I'm writing something I'm gonna share with somebody else. I do it in TypeScript. Not because I care about type safety, but because I care about this autocomplete. I want people to use it without having to think about it. That to me was the real killer feature.
MATT: It's a way of passing on the DX bat to your friends. You get to say, hey, I built this cool thing. You don't really need to looking at the docs to know how to use it. You can just figure it out. Anything you get from it is going to make the rest of your codebase better, easier to use, safer too.
JASON: Yeah. And that -- and there are like, I felt all the benefits of like you write your types and then you change something and then get a type error, oh, I forgot to update that piece of code. I'm not saying you should not care about that stuff. You should. But where it emotionally struck home for me was autocompletion. This is why people care. This is the magic.
MATT: It's the feeling of, oh, my teacher wants me to do well. Sure, they were being hard on me amount the start, but actually they do really know what they're doing. And thank you.
JASON: Yes. Absolutely. Okay. All right. So, what should we do next? Which one should we tackle next?
MATT: Let's have a look. Yes, let's go on to number 3. Number 3 is a good one to introduce some nor generics stuff. Let's expands on what we've got so far.
JASON: This one is absolutely riddled with errors. So, let's take a look. It's expecting that our string set will be a set of strings, set of numbers, unknown set of unknowns and we are seeing -- let's see. We get... we create a set and that's supposed to be a set of numbers. This says we want to pass in a number. So, here. I want this to take a set of numbers so it should return -- okay. I think I have an idea. So, we -- we are passing in our generic here. So, we'll say type. Right? And then what we want is to return. And I'm making a guess... did I do it on the first try?
JASON: Did I just learn TypeScript? What up, chat?
MATT: Trash, this is how you do it. Trash, by the way, this is how you do it.
JASON: Okay. That's -- okay. All right. I'm just gonna take this little clip, I'm gonna put it up on my wall.
MATT: Can I say you not only -- you not only do it first try, but you actually found a solution that I haven't included. so, there we go.
JASON: Okay. So, here was the guess that I had to take. If I tried to use set, it would have a built-in type. That was -- all right. So, maybe let's walk through a couple other ways that we could do this. Because I apparently found something that you didn't do. So, should we look at the solution or do you want to talk through another one?
MATT: Let's look at the solution.
JASON: All right. Here we go.
MATT: The solution is too. What's didn't between this and your solution?
JASON: Ah, so you dropped it in here. Here we're telling the set itself to accept the generic.
JASON: And we're saying what's returned will return the generic?
MATT: TypeScript can do it either way. Refer the return type, or refer it from the return type from what your code does.
JASON: Both of these look pretty clean to me. I think this -- yeah. I... I don't -- I feel like I would have gotten here eventually. But here's the other thing. If this wasn't a built-in type, if VSCode didn't just know what a set was, this would be way easier than trying to figure out how to get a type from the set.
MATT: Yes. There's a couple of wrinkles with this whole setup -- setup. Which is that if you notice that T type here inside -- yeah. Where are we? Yeah. Here we're not actually using it in any of the arguments of the function. So, it's not actually being inferred anywhere. If we were to pass like ATType here. We could actually remove and say a, as string. Hello. And now it would infer string from the argument that's being passed in. So, because we're not passing an argument, we're actually using this generic slot as a kind of -- a slot where you can pass in a type yourself.
JASON: Right and so, what we're saying is you have to decide here.
MATT: Yes. That's it.
JASON: If you don't use it, it will be unknown.
MATT: This is similar to if you have used React. It is similar, you have a use state here, ID, set ID. And you would have to manually pass in here that it's a string. So, generics are interesting. They can be inferred from TypeScript itself. And from just like the call. But you can also override them at the call site and manually pass in your own stuff.
MATT: And Jason, dunk on Trash. Yes.
JASON: I was dunking on the entire chat, Trash, not you specifically. But I mean, I'll -- yeah.
MATT: I got you, man. You're gonna raid us, you got to be prepared for this stuff, you know?
JASON: If you hover over set in the code, it will show you the generics available?
MATT: Yep. That's true. If you -- yeah, command -- and here's something crazy you can do. Try command clicking on set.
JASON: Command clicking onSet. Here we go, here we go.
JASON: I hate it. Okay. Then it comes back. So, if you ever want to, you know, if you do end up being one of those folks who works in an office. I guess the 70% of who are not -- well, not me. I don't work in an office. If you are back in an office and you want to ruin your coworkers day, Matt Pocock has provided the solution. When they retaliate by trying to ship their enemy's glitter, give them Matt's address.
MATT: Come on. I hate getting glitter in the mail. Really rough. Sequins in the post.
JASON: I don't know if there was a service -- there was a service for a brief moment called Ship your Enemies Glitter. They would send an envelope completely full of glitter. I think it was something where when you opened it, it would make the glitter come out of the package. Yeah. It's a full rough.
MATT: But yeah. Well done on Number 3. You smashed it.
JASON: Smashed it! All right. Now -- should we just keep going on 4, 5? Where do you want to go? We've got -- time checking, we've got about 35 minutes left, so, if there's anything you really want to show, let's get there.
MATT: This is lame, but number 4 is good to go to next.
JASON: Okay. Fetchers is an object that you can optionally set keys for the route names. This is the route names, how do but prevent the user from passing fetches that are don't exist in the routes array? Oooo... okay. I've seen a thing in TypeScript that I only lightly understand where people do things like pass in key of or value of. Is that the direction you're trying to go right now?
MATT: I'll give you a clue here. The thing that the question is asking you for is impossible.
JASON: Oh, interesting. Okay.
MATT: What's it's asking you to do is try to find a -- an API that would handle this. Because if we think of a generic, a generic has to exist in the context of a function, right? It has to be something that either you pass in or is inferred and then returns something else. And there's like -- I'm expecting you to the to be able to solve this one, by the way. So, we can like shift over to the solution. This is like an extremely tricky one. This is interesting.
JASON: Wait, wait. No, we can do it. We can do it. If we pass in -- can have MATT: I'm just gonna shake the window.
JASON: Literally leaving me to my own devices. We're about to learn TypeScript with Jason. I'm gonna make it up on the spot. It's gonna go great. What I want to do is if I want to pass in -- if I want to pass in the routes. And the routes are gonna be one of these... pronounced "Trout." But then I would pass in one of these. No. One of these. No. I'm already lost. Okay. So, let me pseudocode what I'm thinking, because that will be easier for me than trying to write TypeScript since I don't know a lot of the syntax. So, what I'm picturing here is that we're gonna return these objects. And so, like each of these would be -- would be like... routes.map then you get your route. And then that's gonna return back... the... return. Right? So, we get our actual route -- or no. It would need to be like an object. Which would be route. And that passes in this function. I did something terrible here. Oh, I've double-wrapped it. Okay. So, this is my -- this would sort of give us the ability to do this and then if I knew that this was gonna a route... and then each of these... needed to be -- oh, oh, oh. Wait. Ah. Ah? No. Not quite. Not quite.
MATT: What you're building here is really cool because it shows the constraints of the problem. If you think about TRoute here... this declaration, this generic, is only valid for this scope. It's gonna be out of scope.
JASON: Oh, it won't come down.
MATT: Exactly. It won't -- like you can't pull it in from outside the function. So, we somehow need like TRoute to be up here in the top level.
JASON: Gotcha. Okay. Okay.
MATT: This isn't valid syntax, right? So, how are we gonna get around that?
JASON: So, now we need to start identifying a type up here. Or I guess -- whatever -- interface. And then... this would be config. Not would take a TRoute?
MATT: Yeah, definitely something there. You're on the right track. Getting warmer.
JASON: Okay. And then I get my routes... which is gonna be... the array of routes. If I'm right.
JASON: Accurate syntax. And then it would be like fetchers. Would need to be one of these.
MATT: You don't need to re-declare the TRoute again because it's already in-scope.
JASON: All right. So, I'm gonna get my routes. Which would be TRoute, like this. And then that gonna return... wait. Each of these would return back... the... oshsh. My syntax is failing me. How would I declare this that is?
MATT: Yeah, this is tricky. What we want to express --
JASON: But that's still wrong. That would let anything come in. Is there a way to get like what was sent here to pass in here?
MATT: Yeah, like --
JASON: Like a value of or -- because what I would do if I was actually trying to check is like, if, route... or it would be... like route -- would have to rewrite some code here. But... you would do something like that to say like if what you're being passed is included in this route up here, then -- then you can go. Otherwise you would need to throw an error. So, I want to do the same thing by getting whatever the value of routes is and then passing that in here. Which is exactly what you've asked me to do and I'm now unable to do it.
MATT: But like, this is hard. Because what we're doing here is we're saying, this slightly changes the API down here. Right? Because --
MATT: Because down below, sorry, in fetches. No longer saying this is an object, it's a function.
JASON: But I'm not allowed to refactor the code.
MATT: You are a bit. But the function is in the wrong place. GetFetchers is no longer needed, basically -- let's, I think. I think if we go to the solution, it's going to elucidate what I'm saying better than I can say it. Because you are super-close.
JASON: K in TRoute! I knew it was possible! Dang it. So, yes. All right. So, this is the -- the value? Is that right?
MATT: This is kind of like --
JASON: X and Y is the -- is a -- is -- so, when you do like an X and Y... right? It would be like that would do the 1, 2, 3, this is the X and this is the Y.
MATT: Yeah, exactly. Sorry if you can hear a storm blowing outside. It's absolutely crazy here. So, yeah. What this is doing is, it's basically creating an object out of the routes. So, if I were to create a type of route, for instance, it's going to be fruits or, sorry, yeah. K in apple or banana and we create a string here. So, we say const Fruit is fruit. And inside here, it's gonna ask me for apple and then banana. And I can add things to this. I can add things to this union. I can say, cheese I know isn't a fruit. But that's the only thing I could think of. And this is basically creating a object from these types here. And TRoute is gonna be a union of the things that we pass in.
JASON: Right, right, right. Okay. So, more practically, what that means is, when we pass this in, right? We get these values. So, what this is becoming... is... this.
JASON: That also works because of the way that we've defined this code. But it's not flexible because, again, you've hard-coded everything.
MATT: Exactly. So, that, then, if we revert that, that TRoute represents the union of those three thing there is. And we can see, though, that it's not quite perfect. Because if you look down at config objects here. And actually, if I add in a -- in fact, this is gonna blow your mind, first of all. If you go into fetchers and try adding a new one to that object. First of all, you can see all the autocomplete in there. Try adding a new one.
JASON: The autocomplete is already amazing.
MATT: And you see all the stuff we've added in there, it's all in there.
JASON: Beautiful. Absolutely beautiful.
MATT: But it's actually not working as well as we think it is. Because if I add something else to this -- if I go like, not a route. Then it's actually going to yell at me. Because it's saying if you hover over that error, not a route is not assignable to thingy, thingy, thingy. Because we're actually passing in the generics up here.
JASON: Oh. Wait. And we can't just leave that out?
MATT: No, if we leave that out, everything falls apart.
JASON: But we -- what if we do... that?
MATT: Then we don't get autocomplete down here?
MATT: Boo! Yeah, quite. The lesson here is generics cannot exist at the scope of objects. They have to exist within a function. So, let's have a look at solution two because I think you're gonna like that one a bit better.
JASON: Okay. So, we've got fig object, TRoute. Extend string. Okay. And now this -- by extending string, we're saying the route has to be a string. That's what we mean when we say "Extends."
MATT: We're constraining it to be a string.
JASON: Got it. Sets up a route here, and route, and allowing that back, that's fine. That's not really what we're worried about here. Make config object. So, we're doing -- we're passing in the TRoute, extended string. And then we need to get back a config object that gets passed a TRoute. Right?
MATT: We're not getting that back, we're actually passing that in there.
JASON: Ah, right, right. So, I see. We're like doubling up here. We've got -- we've got a generic here. But we have to get that generic into here so we're -- we're kind of doing double duty. Do we need a double... the double extends? Or would this one kind of pass through?
MATT: Try deleting -- let's try deleting this one and see what happens.
JASON: Okay. Now it's unhappy.
MATT: And it's also unhappy on Line 13, which is the more crucial error.
JASON: TRoute does not satisfy the constraint. Okay. So, undo that. Now, if I do this... unhappy. Not assignable to type string or number or symbol.
MATT: Only strings or numbers or symbols can be object keys. And we're creating an object out of TRoute there.
JASON: Okay. So, it won't pick up this inference if you pass through.
MATT: Correct. Yep.
JASON: Okay. So, we've got that there. And then, we have this function to make a config object which is more of what we wanted. Which is then picking up all of these. And if I delete this... now it says: Is not assignable to type -- a little messy. But... it does now give us our autocomplete and we didn't have to hard code those anywhere.
MATT: exactly. And if you add a new route, it is gonna give you more things to autocomplete.
JASON: We can get rid of the error... it's cool. And in here... do one of these and there it is. Okay. So, this feels extensible in a way that doesn't -- the thing that always makes me nervous about TypeScript is I don't know how to do this, right? So, the way that I would have solved this problem is by literally hard-coding all of those routes in. And then you have that problem where you've got two versions of your hard-coded routes. It just sucks, right? My way of solving is would be just write the code to do that check in here. Make that a function. Like what I started doing originally. This -- this feels nice. Because it does what we expect.
MATT: Exactly. And it actually lets you pull more logic out of your runtime code into your typing code. We're shipping less logic to the browser or to the whatever because it's more constrained within our type code. And now that make config object, that could live in a library. That could be something you distribute across your organization. And suddenly you have this type safe router fetcher thing that's available to anyone who wants to use it. You notice when we use it in Line 16 onwards, there's no type definitions here. Nothing you need to add to it to make it do its thing. It's doing the inference for free.
JASON: I saw a question from the first time chatter, Charles, the way we're typing on the screen, we're using VSCode Live Share. That's a plugin that you can get... here. Where is the actual -- here. So, this is a pretty cool service. If you've never used it before, definitely give it a shot. Tons and tons of first-time chatters. Welcome, everybody, if this is your first time seeing the show. I'm excited to see you. Hopefully you want to stick around. Remember, you can always, you know, subscribe to the channel, follow, all those sorts of things so you know when we're going live. We have two of these a week. It's a lot of fun. Okay. What should we do next? We have about 20 minutes.
MATT: Let's do one that... um... okay. Let's go to number 8. This is a really famous error. Because it gives you a really hard to understand error message. And understanding why it gives you the error message is really crucial to understanding generics itself.
JASON: That was which number? Sorry?
MATT: Number 8.
JASON: Number 8. Sorry. Let's take a look here. We've got object as const. Interesting. Object key of, type of -- a lot going on in there. So, wait, keyof, typeof object. So, this is the object. keyof, typeof would give us the union of ABC.
MATT: Exactly, hover over obj key.
JASON: That's exactly what we did, great, great, great. And we want to get the object key, TKey extends object key. And then A -- A is not assignable to type T key. A is constrainable to the constraint of type TKey, but TKey could not be initiated with a didn't subtype of constraint...
JASON: So, that means this -- this matches technically. But if somebody put in a B, it would break. Because it would match this but this one is being stripped down to just A, is that correct?
MATT: Sort of.
MATT: I'm gonna let you flail a bit longer.
JASON: Okay. So, let's think a little bit more here... type of 1 by default should be here. But it's not doing it. Okay. So, it's not picking up our default value. And it's not picking up our default value for reasons I don't understand. Because... I actually have no idea. Why doesn't it pick this up?
MATT: This is really brutal. This is like a really hard one. If you hover over one by default there. This doesn't really give you a clue. But it tells you that if we don't pass in our get object key there, it's giving us either one or two or three. Because that's what happening --
MATT: If you hover over this part of it, it will show you that when we pass in nothing to this function, the generic gets locked in as the widest it can possibly be.
MATT: Because you see here, the generic is extends object key, we're making it get object key. It's assignable to, or you're assigning to, then it defaults to the widest it can possibly be.
JASON: Right. Because we don't actually pass this argument, this assignment doesn't happen.
JASON: Is that right?
MATT: It doesn't happen -- and it happens at the runtime level, but it doesn't happen at the type level.
MATT: And so, hover, Tkey, could be instantiated with either A or B or C. And the different subtype of constraint is basically saying, it could be either B or C, and this default doesn't account for that. So, that's really hard to deal with. And there is a trick to do it, basically. And it will get you out of a lot of tight spots. And this always happens whenever you try to assign a default parameter that is more narrow than the generic slot that it's supposed to inhabit. And I can tell by the tone of your voice that you want me to show you the solution instead of like --
JASON: I'm gonna be honest. I don't even know where to start. Because like what -- because like I know what you're saying. We need the generic here to somehow be more narrow. But I don't -- I don't even know where I would begin.
MATT: Absolutely. So, the way to think about this best is if you actually go to -- I think number 2? And then solution 1. You remember we returned to this idea of function overloads. Which is if you call a function in one way, it's gonna return one thing. If you call a function in another way, it's gonna return another thing. Well, one amazing thing about function overloads is you can actually provide different generic parameters based on each overload. So, you can say, I just sort of scaffold some syntax here. We can say, let's imagine that A, a function gets something. We can say, if you pass in, a is gonna be 1, then you return 1. And I have the implementation down here, which is any. And then return as any just to shut up TypeScript.
MATT: So, if we pull at something. Say const result = getSomething. And then if we say -- if we pass in our 1, if we hover over the result here, that's gonna be typed in as 1.
MATT: Sorry, move out of the way. If we add in another, let's say you pass in, I don't know. A, which is gonna be Matt. B, which is gonna be Pocock. And we're actually gonna return here matt pocock.
MATT: Matt Pocock. And then if we wanted one of these signatures to be generic. Let's say I wanted to add someone new to my family. And I say TName. And put in TName here. And TypeScript lets you do interpolation here. Tname Pocock. And I need to extend string.
JASON: Okay. So, we throw in another name. Like...
MATT: Exactly. And we could end up with Robert Pocock. Where if we just add 1 back in here and hover over it, then it actually doesn't even try to fill the generic.
JASON: And if we throw in just the 1, naw, get out of here.
MATT: It's going to try to make it fit. But it's going to error it here, if we try to say, Nancy Smith or something, then that's gonna error too. It should. Not erroring --
JASON: Or just overloads is...
MATT: That's really weird that it's -- I think we found a bug in TypeScript, actually. That's no good. Well, okay. Let's go with that quickly. But the basic idea here is then we need an overload that -- because there's two different cases here. There's the case --
MATT: There's the case where we actually pass something, and the case where we don't pass something and on the type level, those are two very, very different things. So, I'll do some refactoring too.
JASON: Right. So, in this case... we need key A. And that's gonna return A. And then the other one is the generic. But it doesn't like that.
MATT: It doesn't like that. We've kept the default parameter, then.
JASON: Oh, so we can just drop the default parameter.
JASON: Wait, wait, wait. But don't we need that default parameter?
MATT: We don't. There's actually a slight mistake here. Which is we're trying to look at this one here. And actually, in this case, when we don't pass anything, we don't pass any arguments. We shouldn't put an argument here.
JASON: Oh, you're right. You're right. We should -- oops. We're gonna go here. Okay. So, that's happy. That's A. But now everything else is mad because it wants these. So, I have screwed up our overloading.
MATT: We still need to get rid of the default parameter too. We don't need that anymore.
JASON: And now this is unhappy because, not compatible with its implementation. And that's because?
MATT: That's because the way that function overloads work is each one, if you imagine them stacked up, they have to be compatible with their eventual implementation. And the one at the bottom is where you actually implement the function. The other ones are kind of reflections above it that say, you can do this or this or this.
MATT: But the way that we would make these compatible is we would say, you don't have to pass TKey here. You can actually do this.
JASON: And make it optional.
MATT: Make it optional. You know what? I think we need to brute force this a bit. By the way, this return type here, we're not actually returning A. Because we're returning -- we're using the A key to access the 1 here.
JASON: You're right. So, this would instead be a 1.
MATT: And what I'm gonna do instead, I'm gonna just move this one up to here and say this is going to be -- just get this crud out of the way. And the key is TKey. TKey. Then we're going to say, typeof obj. We need to kind of like declare the return type here. I'm gonna call it typeof obj TKey. That syntax there, how scary does that look?
JASON: Looks pretty terrifying.
MATT: What we're doing there, is if we say... let's say we go type whatever equals typeof obj. And pass in key here. What type is that going to be?
JASON: It's going to be a number, right?
MATT: A number or --
JASON: A is gonna be there did -- oh, the literal 2.
JASON: Got it. Nice.
MATT: How about if we pass in B or C here?
JASON: Then it's a union.
MATT: In this case, we know that TKey extends the object, because it's there. And that means that what does this then mean
JASON: This means that basically whatever is passed here, we expect it to be the number literal that's inside. Okay.
MATT: Exactly. Yep.
JASON: Now, it's still unhappy because... implicitly has an any. Can't be used to read-only. Okay. So, the piece that I'm missing here is that I don't know how -- like because we need this code, right?
MATT: Yeah. We didn't -- yeah. We do need this code. What we... what we now need to do is... let's see. Why is this erroring here? String... ah! The reason here is the key here is typed as if you hover over that word there. Is it's typed --
JASON: Why are you a string?
MATT: Why is it a string? How can we make it more narrow than a string?
JASON: One of these, right?
MATT: Yeah, except we don't -- inside the function signature itself, we don't need to re-declare -- the way that function overloads work, you don't need to -- they get sort of wider as they go down, basically. This TKey here, this one is only relevant for this declaration there.
MATT: And so, actually what we want to do is instead of TKey, we just want to put it to the constraint. Which is ObjKey.
MATT: Which is up here.
JASON: And now it's happy.
MATT: Now it's happy. We have manually gone through and specified all the different cases we have there. Before what we had, on the type level, okay, I want you to think about A or B or C, but by default, only do A. And trying to do that in a tiny syntactical space. And TypeScript didn't understand what we were doing.
JASON: Yeah. This is... oof. Yeah, it goes fast. But yeah. There's like a whole... whole chat going on the side here. I'm just checking to see if there are questions. I don't see questions. And chat, if you are able to -- if you have questions, definitely let us know. Are you able to comment out Line 14 and have it work? No. Because we have to have this base case of no argument. Because this one expects that there's an argument. So, we are -- to just explain this back and make sure I understand it, our function here needs to accept a value. A default value is A. And we need to make sure that that is within the constraints of the object key, which is our union of our available keys. And if we add another one down here, this will expand to include D, right? And then so, when we go with zero, zero arguments. We know that we're gonna default to A. So, we need to return that. Because that's what our function does. Right? So, our type becomes 1. So, in this default case here, we know that we're getting 1. And then for all these other ones, our key, what we're sending, needs to be one of these values. And then we are returning whatever the object contains for that key. And that allows us to then set our deal.
JASON: Okay. My brain hurts.
MATT: Let me show you just like -- yeah. This is tasty stuff. Like this is -- there's -- this is like solving one of the biggest pain points in TypeScript Generics, right? It's that instantiated subtype thing. When I Tweet out, what's your least-favorite error message to see? This is the one that people reply with. Trying to work around a horrible error message in TypeScript. And we have a repeatable way to do it now. Which is great. But yeah, I think this is a good place to end on. Because this is the really hard core stuff.
JASON: One more question here, why couldn't we just delete the base case and make the key optional? That's a good question. If we take this, and see if we can do this in 1 minute because we're out of time. So, get object key. And that would be TKey extends object key. And that's gonna take the key which is optional now, and that takes a TKey. And it's gonna return type of object, TKey. And we want the default to be A. Okay. The syntax.
JASON: Okay so, now it's sort of -- sort of happy but it gives us the exact same error.
MATT: This is where we started, basically.
JASON: So, it looks like we have to do this overloading because this inference too hard. Like there's probably a way to make this smart, but like not -- basically, this overload is what makes it all work, right?
MATT: Yep. To answer American250's question too. Another thing you could try and I have tried -- you can actually give -- whoa! No!
JASON: Oh, sorry, sorry, sorry.
MATT: Come on! Is you can actually give this like -- you can give the Generic here -- can you tell I was a voice coach, by the way? Drama. You can give this a default case, right? You can give Generics a default case. Which is really interesting. This feels like it should work. But it doesn't. Because it's still the same problem. You're still coming up against this could be A or B or D or C or D. It's kind of like you're trying to override this with this in TypeScript.
JASON: Hm. Yeah. Okay. So, this feels like one of those cases where like my instinct would be to refactor this instead of trying to -- instead of trying to type it. I would be like, okay. Let's just write this function to be simpler so that we don't have to do this.
JASON: And, you know, it sounds like we're out of time. I'm going to start signing us off. This is the sort of stuff -- are you covering the like -- sure, you can, but maybe you shouldn't part of TypeScript?
MATT: Yes, absolutely. TypeScript imposes API constraints on you. And mostly those are good constraints because they force you to do more functionally. They force you to do more in terms of designing your API so that inference flows through your app. But sometimes they're bad constraints. And so, there are exercises in my course where we look at something that is impossible, you know? And ways to work around it. But also, you should maybe be thinking about designing your API in a slightly different way to take advantage of TypeScript instead of fighting it.
JASON: Right. Right, right. Okay. Perfect. So, with that being said, I just dropped links to Matt's Twitter, to Total TypeScript which is if you want to be further than this, go sign up here. And to the repo that's full of the -- the challenges and solutions. So, go and run through those. This show, like every show, has been live captioned. We've had Amanda from White Coat Captioning, thank you very much. That is made possible through the support of our sponsors, Netlify, Nx, and New Relic, making this show more accessible to more people, which I appreciate very much. While checking out the site, make sure to go and check out the schedule. We are going to reschedule with Will, but I will be on Thursday. We will do something. I have a couple weeks off, a company All Hands and then vacation. And then back strong with a bunch of new stuff. Not all on here, add on Google Calendar, follow on YouTube, all those things so you get notified of new episodes when they're scheduled. And whenever they have gone live wherever you want to consume your media. Any further words before we're done?
MATT: Thank you. It's been a wonderful chat and wonderful audience. Good view numbers. Everybody getting excited about TypeScript. Love it.
JASON: Thank you for hanging out. We are going to find somebody to raid, chat. Stay tuned -- Ben is live right now. We're gonna go raid Ben D. Myers. Thanks all for hanging out. We'll see you next time!