Make the Fetch API type-safe with feTS
The Guild's feTS lets us build REST APIs with end-to-end type safety using TypeScript and the OpenAPI Spec. Maintainer Aleksandra Sikora will teach us how it works!
Links & Resources
Click to toggle the visibility of the transcript
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 have Aleksandra Sikora. Aleksandra, thank you so much for taking time to hang out with us today, how are you doing?
ALEKSANDRA: Hi, everyone. Thank you for having me here. I'm super-excited because I wanted to be on your stream for such a long time. I'm really excited that it's happening today.
JASON: Yeah. And I'm really pumped because we're touching something near and dear to my heart which is plugging different services together in a way that is actually fun to work with and so, before we talk about all that, let's talk a little bit about you. So, for folks who aren't familiar with you and your work, do you want to give us a bit of a background?
ALEKSANDRA: Yes, I'm an open source developer at The Guild. I work in the space and also on other things like feTS that we're gonna learn about today. And previously, I was working on a framework, Release.js. And I was working on a console, Hasura, before. So, I have a bit of full stack developer background around TypeScript mostly.
JASON: Great. So, working with TypeScript, working with GraphQL, there's kind of a common theme of trying to get data into a shape that I guess is pleasant to work with as a developer. How did you -- I guess what was your path that got you to GraphQL? Why did you land on it as a focus area?
ALEKSANDRA: So, I started working with GraphQL when I joined Hasura. I was not that familiar with GraphQL before joining. But I think it was a time when it was kind of more and more popular. Like a few years ago. And this is how I learned GraphQL. It sounds very -- you know, very compelling for me like from the frontend perspective to be able to, you know, get all the data I want without the over-fetching or under-fetching problems. So, that was a huge deal for me. Then I kind of stopped working in the GraphQL space for a while. I went to RPC. A part of the API world for, I think, almost 2 years. And now I'm back. And I think there are still like amazing things to do in the GraphQL space. Especially regarding type-safety. I was mostly working on the GraphQL generator, this thing that gives you type-safety for your consumer-side of the project.
JASON: Cool. Yeah. Okay. So, that's actually kind of -- I -- I don't want to over-focus on this, but I'm always really curious about with something like GraphQL, I think there was this huge hype early on. What was it? 2017, 2018 --
JASON: Why GraphQL sort of had this massive wave of popularity and it was kind of being touted as the future. And we saw a lot of companies get built up around GraphQL. Most notably was Apollo. Apollo became the center of the GraphQL universe for a center. And then we've sort of seen this shift back toward people are doing REST APIs, we're looking at tRPC. We've got these sort of -- well, you don't need GraphQL, you just need type-safety in your fetch calls. And it sounds like you're kind of living in both worlds. You're looking at the --
JASON: The sort of tRPC space -- the RPC space, not just tRPC, and working with GraphQL and you're working with a lot of TypeScript. How do you see GraphQL having settled into the landscape? I would say its hype peak is over and it's sort of reached the level of being a tool now. How do you feel it fits in? And like where is GraphQL really useful in a Dev stack?
ALEKSANDRA: So, where does it fit? So, I think that, you know, comparing like RPC and GraphQL and REST kind of doesn't make sense, you know, in general. To just talk about it like, you know, in this abstract way. It all depends like in the -- of the context. It depends on the team, of the people who are gonna work on those APIs. On the project where we're gonna use those APIs. So, I think there is a really huge use case for GraphQL in -- like companies that want to have like maybe public APIs or maybe they have like separate clients. And they -- the clients deal with extracting a lot of data. And where, for example, tRPC is really great is for rather like smaller to business -- smaller to medium applications where like the speed of development is crucial. And where, you know, coupling several end clients isn't an issue. Like we want to ship fast. We are working, for example, on a SaaS application. We want to be able to like create backend along with writing frontend for this speed. And that is very compelling and that gives you tons of benefits. But it can also be limiting. Like know when your backend grows, then maybe you want a separate team. Or maybe you want a separate clients to access your backend. Like maybe now you have a mobile app. Or maybe you have another app. So, now you face a problem of exposing this -- this backend API to multiple clients. So, then we maybe we want to, like, revisit like whether GraphQL or REST could be a better idea for that. And it's not like those things are not possible with tRPC. Like everything is possible. But it's like some tools make some things easier than other tools.
JASON: Right. Yeah. I mean, everything is a tool. And you got to look at the problem you're trying to solve. Or you're gonna end up, you know, trying to force a tool to do something that it's not built for. No, I love that. Okay. So, all of is that, I think, is good foundation to then introduce the -- the central topic of our show today which is feTS. Which --
JASON: Did I pronounce that properly.
ALEKSANDRA: Yeah, I saw a question from someone how to pronounce it. Yeah, it's feTS. The name is from Fetch and TypeScript. So, like f-e-t-s.
JASON: Yeah. Let's talk about that a little. At a high level, what is feTS?
ALEKSANDRA: Yeah. So, feTS is something that I'm to bring type-safety and type-safety for REST APIs. Because let's say we ended up with working with a REST API. Maybe we are creating a new REST API. Maybe we already have a backend that's created the REST API. Or maybe we are dealing with some legacy REST API. Then how do we have the type-safety on the like consumer parts or on the client? Like we still want -- we don't want to, like, go to the commentation every single time and we want to have some guarantees about the APIs at the build time. We want TypeScript to tell us, oh, this is the wrong usage of API. We have two options. We can manually type it. We can manually type all of the responses and their requests parameters, how those objects should look like. Or maybe we can generate it from the Open API Spec. And, you know, as you can guess with type it manually, it can be, you know, a lot of work.
ALEKSANDRA: It can also get out of date. And to -- like, again, you have to look at the documentation. Like you have to have it in front of you and then basically write it down on your -- on your machine. And with that code generation, this is like an additional step. You have to, you know, add it to your CI or to your like, you know, the development process. So, with feTS we wanted to solve this problem. And we wanted to give you something that automatically infers the type from the specification of your REST API. Without any manual typing, without any code generation. FeTS takes the type of your Open API Spec and then infers what are all the endpoints? What are the like body parameters types? What are the response types?
JASON: Got it. Okay. Cool. And this is -- this is something that I think is -- is very, like, relevant to my interests. Because I know my -- one of my first use cases for GraphQL is when I was at IBM. And the way that we were trying to use it was as a -- a normalization layer over a bunch of backend APIs that they didn't know how to communicate to the other teams. There were dozens of teams, right? And so, they didn't know who needed to be updated, who was using the data. So, we added GraphQL at this middle tier to normalize access. And the way that we had to do that is -- you're gonna laugh -- I had to call each of these APIs and see what the response was. And make sure that that response matched what we had written in the GraphQL schema. So, that we -- it was just this very --
JASON: Continuous, manual triage of making sure that the types lined up. Because we didn't have any way to automate this. Like there wasn't -- there wasn't a CodeGen step. And this was early days of GraphQL, before any of the tooling had been built around it. I was hand-rolling resolvers to make this work. This is a very welcome space for me to hear that people are focused on. Because we all like to think about tools in the Greenfield state. Well, sure, if I controlled all the variables, then tRPC is wonderful. But in any team of size, that's not the case. You don't have access to the database and, you know, the server and the frontend and all those things just to do whatever you want. You have too many teams, too many people who need to depend on that stuff. So, this is exciting. To repeat it back you to make sure I understood it. FeTS is going to use the Open API Spec to automatically give me type-safety on the client-side with any REST API that has an Open API Spec?
ALEKSANDRA: Yes, exactly. Without code generation and also your schema. Everything from this Open API Spec won't end up in your bundle size. Won't increase your bundle size because -- because it's just type. So, it's gonna be stripped from the bundle.
JASON: Very cool. Very, very, very cool. So, is there -- do you have like -- this is the ideal use case if somebody wants to starts working with feTS. If you're looking at this, you should pick it up right now? Or is it like -- is it more broad than that?
ALEKSANDRA: So, I have a few use cases that are interesting and I would like to tell you first about the use case that I think inspired Arda from The Guild who created feTS. To create that. It was when they were working with some clients and they were creating like a GraphQL servers. But they were writing resolvers. And under the hood there were like a bunch of REST APIs. So, in this middle layer, in this like back and forth frontend part, they needed some additional type-safety. And this is when Arda came up with feTS. So, this is one use case. We don't have to look at feTS from the like frontend perspective. It can also be like the middle layer. Maybe we can use it like to create our GraphQL resolvers. Maybe we can use it to create some SDKs that then we're gonna expose to end users. So, this is one interesting use case. Another one that is -- is also very cool is that, for example, you are dealing with a legacy API. Like there is some REST API, they have the Open API Spec, but there's no SDK. Or maybe there is, but it's outdated and maybe it's heavy like bundle-wise.
ALEKSANDRA: Then you can take this Open API Spec and use it on your -- like on your client with like -- you can have all the type-safety.
ALEKSANDRA: And I think this is something we're going to explore more later. And another thing is -- it's also like slightly in the same space. Maybe you have existing REST service. Like your backend has the REST service and you want to -- like you want to have type-safety on the client. So, this is also where feTS is really good at. And I am to mention here that feTS is actually two parts. We have the client part and the server part. We're going to talk more today about the client part because for me, it's like the -- like the most exciting thing about this project. But there is also a server that lets you create REST APIs very easily. It's kind of similar to tRPC. Like if you have experience with tRPC and with how you write the procedures, then it's kind of like similar developer experience. But it gives you like the Open API Spec out of the box and then you can also easily use it with -- with the feTS client.
JASON: Very cool. And so, does that mean that -- that we can also use tRPC with feTS? Like if we had tRPC on the backend, we can use the feTS client on the frontend to consume it?
ALEKSANDRA: As long as you use this Open API plugin for tRPC, then, yes, you can use it with feTS.
JASON: Very, very cool. Okay. So, I have a million questions, but I have a feeling we're at the point where it's gonna be easier to show me than tell me. So, why don't I switch us over into the pair programming mode. And here's the button for that. Okay. All right. So, this episode, like every episode, is being live captioned. We've got Amanda here from White Coat Captioning, taking care of that, thank you so much, Amanda. And that's made possible through the support of our sponsors, Netlify and Vets Who Code. Thank you so much for supporting the show. And we today are talking to Aleksandra. Is this the right place to send people? I don't know if Twitter is the right place anymore.
ALEKSANDRA: Yeah, I think so. I mean, I tried to switch to Bluesky. But I'm in the very consistent with that. And I really, really wanted to use Threads. But it's not in Europe yet.
JASON: Oh, really. Oh, that's right, I saw that. Yeah.
ALEKSANDRA: Yeah, the regulations.
JASON: All right. So, then I have the feTS project here. I'll drop a link to that. And okay. If I want to get started, what should I do first?
ALEKSANDRA: Okay. So, I think we can go to the client part of the docs.
ALEKSANDRA: And here is this -- this quick start guide. And I was thinking that maybe we can create a small project. So, like what's your favorite thing? What's your guilty thing when you are creating a new frontend project?
JASON: Typically I'm using Astro. But I can work with whatever we -- whatever -- I tend to get a lot of practice with frontend frameworks so I'm pretty happy to do whatever.
ALEKSANDRA: I think maybe we can just go with Vite. It's very minimal. And we can use this, I think, React TypeScript template.
ALEKSANDRA: Yeah, someone pointed out that Astro is basically Vite. We're going this one layer down to having something very, very minimal.
JASON: So, I am installing. And then we can -- let's open up the sidebar. No, wait. Let me actually open this project. And it -- GitHub here. Okay. So, this is our project. And I'm going --
JASON: Get init in here so that it stops hiding everything. We have our standard, bare bones Vite setup. So, we've got our basic app. Do you want me to leave any of this? Or should I strip it down to --
ALEKSANDRA: We can leave all of that. We're gonna work with that later. So, maybe after you have the components installed, we can add feTS to the dependencies.
JASON: All right. That's where we started. That's what we're looking at right now. So, I will npm install feTS. Okay.
ALEKSANDRA: Okay. Awesome. So, if we go back to the docs, and to look at the next steps of -- I really like that you are using Arc. It's my favorite browser.
JASON: I do like it. I seem to have cleared my window. Here we go. Back at the docs. So, I've installed feTS.
ALEKSANDRA: Yeah. The first use case here is usage of existing REST API. So, for that, what we are gonna need is the Open API Spec. And I was thinking that we can build something with a Spotify API. So, first thing we need to do is to find the -- the Open API Spec. Yeah. I hope this is the one.
JASON: Okay. So, this is a Spotify API. And let's see --
ALEKSANDRA: Yeah, I think if you Google -- if you Google Spotify Open API Spec... yeah. Let's open the -- yeah. I think I was there under this link previously. And then you can -- somewhere at the bottom, there was a link -- oh, yeah. Yep. This one. Awesome.
JASON: Here. Okay.
ALEKSANDRA: So, right now we have the Open API Spec for Spotify. But it's in YAML. And we would like JSON for TypeScript to work with the JSON object. So, you can copy it. And -- well, right now we can just use an arbitrary tool to convert YAML to JSON. But we can also -- yep. We can also already open the Swagger editor, and -- yeah. It's the Swagger UI. Yeah. I think let's go to the first one. And actually, click on this button in the top right corner. This "Try new one." Yeah. It has more options.
ALEKSANDRA: So, here you can paste the spec. And in the edit, there is this edit button at the top. And you're gonna see some options and you can convert to JSON. So, you can like -- it's like two in one. You can convert it from one format to another and you can also like play with it. See what -- what's inside.
JASON: Very cool.
ALEKSANDRA: Now we can take it. This specification. Go back to the editor. And we can create a new tile. It doesn't matter right now where the file is. Let's call it OS -- or Open API Spec.
JASON: Open API Spec.json.
ALEKSANDRA: No, actually, it's not JSON. Because currently TypeScript can't infer types that come from like the JSON. We would like it to store it as an object. So, let's --
JASON: Oh, I gotcha.
ALEKSANDRA: Let's change it to a TypeScript file.
JASON: Okay. So, let me get up to the top here. And then we're gonna export -- like export default?
ALEKSANDRA: It can be default. It can be a const. It doesn't matter. It can be a default or a named export. And what's also important here is that at the end we're gonna ask const assertion so that TypeScript can narrow like the most specific type as possible. So, it won't be just any object with like -- with like some properties. It's gonna be exactly this.
JASON: I think my VSCode is struggling a little bit because this is a pretty big file. It can't even -- there grow. There you go.
ALEKSANDRA: That works.
ALEKSANDRA: Okay. So, let's do this as const at the end of this object. Oh, yeah, you --
JASON: I think I got it.
ALEKSANDRA: Already. That's awesome. Okay. So, now we can go -- we can write the code like create this new client in the like the up entry point. Or we can create a new file. Maybe for the sake of, you know, having this very, very clean, we can create a new file. Like let's call it client ts. That's client ts. Okay. And in here, we're gonna import create client from feTS.
ALEKSANDRA: And also from feTS, we're gonna import something called "Normalize OAS." Yeah. This is the first one, the type.
ALEKSANDRA: What it's gonna do, it's gonna help the feTS kind of figure out the types. It's gonna normalize some properties in the Open API schema.
ALEKSANDRA: So, right now let's do -- let's do const client. Yes, create client. And we're also gonna pass a generic parameter to the create client function.
JASON: Oh, right, got it, right.
ALEKSANDRA: Yeah, TypeScript generate, right, here. And here we're gonna use the normalizeOAS. And here we add in the Gen rate parameter. And here we're gonna do the type of Open API schema object.
JASON: Got it. Okay and that one we'll have to import. Okay.
ALEKSANDRA: Yeah, yeah, exactly. Yep. So, there are a bunch of config options. But what's like the -- like the most important is providing an endpoint. So, I think to see what the endpoint is exactly, we can go to the specification and it's gonna be there somewhere. Oh, yeah. This URL -- line 14 -- yep.
JASON: Right. Got it.
ALEKSANDRA: So, we have the client. We can like already play with it to see if there are like they're the proper types. We can see what are the endpoints.
ALEKSANDRA: Maybe we can switch to the docs to see how to -- because I think there are nice examples there.
JASON: Yeah. Let's get here. Okay.
ALEKSANDRA: Okay. So, you see that -- yeah. Those are all like async functions. It basically is client. And then you select what endpoint you want to access. And then depending what are the available metas on this endpoint, whether it's just get or whether this endpoint has get, post, output, delete, then you're gonna have it available on that object.
JASON: Got it. Okay. Very cool. So, then in here, I can get, like, or do I need to wrap this in a -- do we get top-level await in Vite? Go client. And then... what was here? A dot? Yeah.
ALEKSANDRA: Oh, yeah.
JASON: Here is all of our -- this is so cool.
ALEKSANDRA: Yep. And then you're gonna also have like other parameters typed properly. TypeScript will tell you, oh, it didn't pass to this endpoint that's required. An ID parameter.
JASON: That's so cool. All right. I love this. So, we can get an artist ID. Right? And a Spotify artist ID. I think you can -- can you get those out of like Spotify itself?
ALEKSANDRA: Well, I was -- I was trying to figure that out. And what I -- what I kind of figured out is when you click, like, some share -- yeah. There's copy link to... yeah. And when you -- like in this link, there should be some kind of ID.
JASON: Share thing. And this is the ID. So, we can take this and then I need to... or, wait. How does this work?
ALEKSANDRA: Okay. Yeah. Now I think it's a get. Yeah. And inside, we --
JASON: Parameters --
ALEKSANDRA: Pass an object. Yeah. And we have params.
JASON: Params. Oh, it knows! Oh, that's amazing.
ALEKSANDRA: Yeah, it knows already everything. But what else it's gonna need is an API key which I'm gonna send to you to save you from, you know, generating the Open API key to help us move faster.
JASON: So, I will create a dot end because Vite will pick that up for us.
JASON: And then I'm going to go off-screen here and let's say --
ALEKSANDRA: Well, it's gonna end up in the browser anyways. And it's gonna expire in 20 minutes or so.
JASON: If we're cool with sharing it, then I can --
JASON: I can just do that.
ALEKSANDRA: Yeah. And also in Vite, if you want the API -- your end variables to end up in the browser, you have to add this Vite app on purpose.
JASON: Yeah, like Vite public or something?
ALEKSANDRA: Vite_up_and then like the name of the variable.
JASON: Okay. So, should I -- do you want me to put it in a nvar, how do you want to do this?
ALEKSANDRA: Yeah. We can put it in the nvar or place it directly in the file. Maybe we can put it in the nvar so it's easier to reuse it. It's easier to copy/paste. Yeah. And now we're gonna have headers here.
ALEKSANDRA: Because also right now I think TypeScript complains about the lack of headers. Because Spotify requires us to always send the authorization header.
JASON: These in there and then... authorization header. And we can go import.meta. Vite API key, I think is what I called it?
ALEKSANDRA: It's not up, like up. But a-p-p.
JASON: Oh, app! Sorry, my bad. Oh, I haven't seen this approach before. That's because I was making things up in my head.
ALEKSANDRA: I mean, I --
ALEKSANDRA: It works because it is take this up to my browser.
JASON: That's what I thought. That's what happened in my brain. Oh, that makes sense. Leveling up to the browser. Okay. Okay.
ALEKSANDRA: It makes more sense than the a-p-p. Okay. So, we have our call. And now, well, what we want to do is probably --
JASON: And I just want to take a second to just like -- I have not looked at the Spotify API, right? So, I got that API spec, right? And then I have been able to like control space my way through this entire process. I found the endpoints by looking at -- I hit dot. And it shows me all the endpoints. I hit this .get and then it showed me the headers and parameters. It knew the API and I'm autocompleting myself through the Dev process. I'm not having to refer to the API docs at all. I'm getting to where I need to go. This is really, really cool.
ALEKSANDRA: Yeah. It is. And I think the hardest part was to figure out was the artist ID to go to Spotify and take it.
ALEKSANDRA: Yeah. I just wanted to mention that we forget about it.
JASON: Okay. So, we have a response. We -- we've -- we caught this before it broke. So, you know, give ourselves a little pat on the back there. And theoretically speaking, we should be looking at an artist once we -- once we, like, echo this out. So, should I --
ALEKSANDRA: Well, actually, first we need to see if the response is okay. So, if there weren't any errors. I think there's gonna be -- if the response -- there could be a OK on the property. Okay. And then we also -- not yet. Not yet.
JASON: Oh, not yet.
ALEKSANDRA: So, we also want to do -- to call like JSON on this response. Which is also gonna be async. So, let's maybe do const artist. That's await and response JSON. Yeah.
JASON: And give us our artist. Okay. And so, we'll just dump an error if something goes wrong here.
JASON: K. So, this should make it all work. And I just realized we don't have any way to call this one. So, we can --
ALEKSANDRA: Yeah. Yeah. But basically if we import the client in like the entry point -- oh, yeah, that could also work. If we add it to the app TSX.
JASON: Yeah. So, I'm just gonna import our client. And then we can put all of this right here and it should work, right?
ALEKSANDRA: Yeah, yeah.
JASON: The component? And then if I go back out to our app...
JASON: And --
ALEKSANDRA: Yeah. It worked on the first try.
JASON: Ta-da! This is pretty dang cool.
ALEKSANDRA: The worst part is over.
JASON: All right. Let's try this. Let's just swap out the artists and show it again. Everybody can judge me for what I like in my Spotify. Let's get one of these. And then we'll come back here. Nope, that's the wrong window. Let's see. ID goes there. And we get... Fleetwood Mac. Super cool. That is definitely -- like I have used the Spotify API before, and that's the fastest I've ever used it. So, that --
ALEKSANDRA: I'm really glad.
JASON: There's a big vote of confidence for the fact that this is an API that I don't know. It's an API that I didn't look at docs for. And I was still able to successfully make a call just kind of intuiting my way through the autocomplete. I think that's a really good sign that you've got a great tool on your hands.
ALEKSANDRA: Yeah. You don't even need me to help you with that. You only needed TypeScript and the autocompletion.
JASON: While I think that might be technically true, we would still certainly be fumbling through it if I hadn't had -- if I hadn't had the adult supervision there. I mean, this is great. This is -- this is really exciting, really cool stuff. And I feel like we're just barely scratching the surface.
JASON: So, what else can we do with this?
ALEKSANDRA: Okay. So, currently, like what you see that we just at a discount like imperatively to our entry point. Maybe we can build a small UI and see how to use it in like a real-life example.
ALEKSANDRA: Like if you are actually building the app, like the UI from the API. So, all of this code you have, maybe we can put it in a function. Like some kind of like fetch artist or something like that. And then we'll call it in the app.
JASON: So, we will do a fetch artist, we'll pass in an ID. And that will be these. And then we'll return the artist.
ALEKSANDRA: Yeah. I think we also need to add async.
JASON: Yes. Wink. Okay. So, we have our -- we have our async function. Let me make this a little bit... what am I doing? Make this a little bigger. And we've got a little action we can call to fetch an artist. And then we will swap this out to be the ID.
JASON: K. And feTS is happy because the types match. And I'm glad because I guessed. So, now --
ALEKSANDRA: Even if you guessed like wrongly, then it would tell you right away what is the correct type.
JASON: Ah, that's true. Watch! Now, this should yell, right? It does.
JASON: That --
ALEKSANDRA: There was this one second or two second when it didn't and I was like, hm. Do we have a backup?
JASON: That's always the best one especially because my VSCode is such a train wreck because I add new things to it every week. Every once in a while it just won't do a thing. And it's like, huh? Is that the software or is that I've lit this computer on fire?
ALEKSANDRA: Yeah, yeah, I have this problem they usually have like 10 VSCodes open. So, sometimes it's not the fastest.
JASON: Okay. So, we have the ability to fetch an artist. I have -- I'm gonna just make sure I don't lose that in my copy-paste there. That's -- that one's Fleetwood Mac. And then I can grab a few others if we need them. And then, you know what will actually be fun? What if the thing we build here is we can get a playlist and show the image for each of the artists -- we can limit it to ten or something so it's not too over the top. The fact that I just thought about that, and I don't want to figure out all those APIs. No, this will be easy.
ALEKSANDRA: Yes, yes, it's going to be very, very easy. You're gonna have like what's the response -- shape of the response right away.
ALEKSANDRA: So, that's gonna be okay.
JASON: We'll be able to do a playlist, right?
JASON: And then I can come in here and I can get playlist. So, I just know to go down to my playlists and get an ID. And I want to get -- it's gonna give me an ID. And then this one doesn't match because this one --
ALEKSANDRA: playlist ID.
JASON: playlist ID. Easypeasy. All right. We'll make this one match. So, now I've got a playlist ID, and everything else just works.
ALEKSANDRA: Yeah. And we can also -- if the response is not okay, we can throw an error so that we don't go to like this path of calling the JSON on the response.
JASON: Okay. So, I'll throw an error instead of console logging an error. Oh, it needs to be the -- so we can do like the response.text, I think it is? This needs --
ALEKSANDRA: Yeah, I think it's a --
JASON: We'll at least get some information if it goes wrong and we can dig deeper.
ALEKSANDRA: It won't go wrong.
JASON: It's not gonna break.
ALEKSANDRA: If it will, then it's gonna be Spotify's fault. Not ours.
JASON: Exactly. Exactly that. Okay. So, then what we can do is in here somewhere, I want to just make a list out of this. So, we'll get rid of these and we can say, like, my playlist rules. And then I want to...
ALEKSANDRA: Oh, you know what we can also do? There is this -- I think there is an iFrame link in the responses. So, we're gonna -- we can also like put an iFrame with the like preview from Spotify.
JASON: Oh, cool!
ALEKSANDRA: So, someone can play it like right away in our application.
JASON: Okay. So, let's add iFrame. And then we will... we'll just list artist -- okay. So, we can do -- we can do something like to. And we can go further if we wanted. But I don't think we'll -- we'll see. But so, then if I want to do that, then I need my -- my playlist. So, we would do -- do I want to do this in a useEffect?
ALEKSANDRA: This is a question for me. Do you use React Query or something similar?
JASON: If I'm using it in multiple places, I'll use React Query, otherwise I just use the Fetch API.
ALEKSANDRA: Okay. Then we can have like use state with like the playlist, we can save it in the state. And then we can have useEffect to call the -- do like execute our fetch playlist function.
JASON: All right. We'll do a playlist is a set playlist, and then use state.
ALEKSANDRA: But if you would want to use React Query, it would also work very well with feTS, just saying.
JASON: Okay. Yeah. I do -- I mean, I am a very big React Query fan. I think it's a very well-done piece of software. Let's do -- so, we'll do an async function. That would be like a load --
ALEKSANDRA: Oh, so, we don't need to create a new function here. We could just do fetch a playlist and then we could also add catch.
JASON: Right. You don't have to do everything as async await. You can just use -- you can just like, you know, use a platform. So, we'll come up with a -- an ID for that. But then we can .then. And with this... it will be our playlist. We can set playlist. And something's wrong... we can throw new error. Okay. So, what are you unhappy about?
ALEKSANDRA: Yes. So...
JASON: I think I missed something.
ALEKSANDRA: I think there are gonna be a few issues. And the first one is that TypeScript is not happy that you assigned this playlist to an area with never. Because whether you don't provide a type for the use state, I think it's -- oh, actually, it's undefined because we don't have an area here. Yes. We still need a type because right now it's gonna be an array of never. And we're gonna pass a generate type to the useState function. And right now -- so, what is the type of the playlist? Like we could possibly do some kind of like type of -- like return type of the fetch playlist function. Like we could do this on the TypeScript -- like with the TypeScript utility. But we can also use feTS for that. So, if you could go back to the docs for a second. Because I'm not sure if I remember correctly how this utility type is called.
ALEKSANDRA: Okay. And now if you go to the... yes. Inferring schema types.
ALEKSANDRA: So, we have a few -- a few utility types here. So, we have models. Oh, and here we have response body. So, we're gonna go with that OASoutput. And based on the endpoint that we used, it will tell us what's the type of the playlist.
JASON: Cool. Okay. So, I go -- okay. We can do this side-by-side. I want OASoutput which I need to --
JASON: And then that gets normalizeOAS.
JASON: And then we do the type of Spotify.
ALEKSANDRA: Yes. And then we also pass the -- the playlist.
JASON: All right. And that is a get.
ALEKSANDRA: Okay. Is there something underlined in the type?
JASON: Only refers to a type, but is being used as a value. What have I done?
ALEKSANDRA: I think we are missing one -- one -- like the strangled bracket there? Don't we? Or maybe we just go replace from the docs.
JASON: Okay. So, we've got this one. And so, I need to grab this. And put this here. And then one is going to change Spotify.
ALEKSANDRA: Yeah. And at the end, we need one more -- oh, yeah. And also at the very end. Or not. No. No. It's -- it was fine. Right?
JASON: Let's see. One, two... two, three... yeah, we're short one.
ALEKSANDRA: Okay. I have another idea. Let's create a new type. Type playlist. So, we're gonna save in a different place.
JASON: Okay. And then use this here?
ALEKSANDRA: Yes. So, it's -- it's also gonna be a shorter and then we can also use this type.
JASON: Okay. So, then we can do playlist and we need the playlist ID. Oops. Like... playlists.
ALEKSANDRA: Yeah. Playlist.
JASON: You know what would be smarter is just copy-pasting that? Great.
ALEKSANDRA: And then we have get... oh, it's a small get.
JASON: Lower case, get or put.
ALEKSANDRA: I think it's just your VSCode is slow. We're good.
JASON: Then the 200... and that does --
ALEKSANDRA: I think it expects a string.
JASON: Oh, got it. K. Then you'll become --
ALEKSANDRA: Okay. And now we can say playlist. Yeah. It's a single playlist. It's not a list. Okay. Perfect.
JASON: And now you're happy. Whoo! We did it. Okay. I love TypeScript. No, I get -- like I get it. I get how, you know, this is -- this is definitely the part that is I think the trickiest for me is when you start getting to the point where you're like, okay. So, I'm writing custom code based on a thing and it's all figured out in the code. But I need to extract this return value out of somebody else's code so that I can then use it like --
JASON: It's very -- there's a lot of -- a lot of like head-bendy stuff that happens in there. But we got there. And it ends up being a one-liner, which is nice. So, then -- I guess it autowrapped to being a five-liner, but that's okay.
ALEKSANDRA: Well, it's fine.
JASON: So, now we should have a playlist if we go and get a valid playlist ID. So, let me switch over to -- to here and I'll go to playlists. If I can find 'em. How about this playlist? And then we can --
ALEKSANDRA: Yes, I really like this one.
JASON: We can share it. And here's the playlist ID. Okay. So, then we'll drop that one in here. And then we can get rid of this. All right. So, first and foremost, maybe we just dump this and make sure that it works.
JASON: We'll do one of these. JSON stringify it. And... this goes here. Okay. It should, theoretically, do what we want.
ALEKSANDRA: Yes. It should.
JASON: Give it a shot. And if I close this.
ALEKSANDRA: It's not the prettiest website. But... we did get what we wanted.
JASON: But it did the thing. It did the thing and this is amazing. So, then once we get in here, we have -- we've got the playlist name, we've got the -- this is not formatted in a way that's easy to read. But we've got the tracks.
ALEKSANDRA: I think there's some kind of like justify items center or align items center going on.
JASON: I wonder if I turn this off if it will fix all of these things for me. Might need to wrap...
ALEKSANDRA: Yeah, fragment.
JASON: What are you doing? Stop with your helping, you.
ALEKSANDRA: Oh, yeah!
JASON: Now it's doing what we want.
ALEKSANDRA: Too much CSS.
JASON: So, we've got our tracks. The tracks have items. Each of the items includes the -- the track name and the artists. And so, it will just be I guess grab the artist.
JASON: And this even gives us the ID. So, theoretically speaking, we could -- let's see. What would the right way to do this be? Just from a software architecture standpoint? Should we loop over these in the playlist to like get the details about the artist? Since we don't have an image? Or... do it out here?
ALEKSANDRA: Well, if we don't care about any other data, we can -- we can like filter it out earlier. So, I would like leave the fetch playlist function as it is. Because, you know, we probably want it to still like fetch the playlist, not modify the data. But we can -- we can filter it -- like we can update our state variable to only store like not the whole playlist. But the things that we care about.
JASON: Okay. Yeah. So, what do we care about? We care about... so, let's think about like what we -- what we want. We want the playlist name. We want the tracks. That would be an array of stuff. And then inside of that, we want probably the track name... artist, name, and like an image or something -- URL, I think. We wanted that iFrame thing too. Where is it? Where is that?
ALEKSANDRA: I think there was something like external URL? Oh, this thing that I think the one that starts with Open Spotify, I think that's the one that opens the iFrame. But I'm not sure. I think we can try it out.
JASON: So, I think this one -- yeah. Let's see what happens. I think this one will attempt to open the app.
ALEKSANDRA: Okay. I think I know. There should be like an and, but inside the URL.
ALEKSANDRA: Is there something like that?
JASON: Let's see... iFrame, preview, preview URL.
ALEKSANDRA: Maybe this one?
JASON: Is that for each track? Okay. So, if I do one of these... then what does that do? [Music playing] Oh, okay. So -- so, that -- that's the preview URL.
JASON: But that's for one song. Let's see... is there a -- hm. Maybe that's something that needs to be done with a little extra code.
ALEKSANDRA: So, maybe let's take to displaying the data and maybe like adding a link to Spotify. Maybe we can add a link like go to Spotify. Like open in Spotify based on this -- this URL with like this open.spotify that come.
JASON: Okay. That worked. So, this is -- this should be enough for us to -- now I guess we can do a link to the song also. Okay. Any other data that we want?
ALEKSANDRA: I don't think so. No.
JASON: All right.
ALEKSANDRA: We have name, we have artist, yeah.
JASON: All right. So, then we can just add a little link in the chain here. Where we get our... now, this is gonna break the typing that we use for the set playlist. Because we're gonna be filtering it. But we're gonna say filtered. And let's say playlist. I'm gonna be like that. It's gonna be...
ALEKSANDRA: So, okay. Yep.
JASON: Get the name. And then we can get the link would be playlist: External, I think it was? Spotify.
ALEKSANDRA: And I think there's Spotify. Yeah.
JASON: Then we need the --
ALEKSANDRA: And the tracks. So, we're gonna map through like the playlist.tracks.
JASON: That's so --
ALEKSANDRA: And it will also complain about the undefines. Because I think everything in this schema can be undefined.
JASON: Got it.
ALEKSANDRA: If we also wanted to like make types for this playlist data.
JASON: And we want to map and for each track, we want to return... the name. Which is the track.name? Did I lose my track types? Here it is. track dot added -- track.
ALEKSANDRA: Yeah, here we go. Name.
JASON: I'm gonna rename this to item so that it's -- okay. So, we got our name. Make this bigger. We've got an artist. This is the one where we're gonna need to fetch the artist. So, we will... let's see... const artist equals await, fetchArtist and we'll get item.track.artist. Are you with me still?
JASON: Artist, zero.id.
JASON: And then we should be able to do artist.name? I think? Yes? And image will be artist.images.
JASON: And zero dot...
ALEKSANDRA: I think it's just -- yeah. We have URL.
JASON: These. And then for the link, we want the item.track.external_URLs, I bet. Spotify. And what are you mad about?
ALEKSANDRA: Oh, yeah, this is undefined. And the items is possibly undefined. So, yeah. Everything can be undefined in this -- in this response. So, we can also -- yeah. We can add some holds.
JASON: Yeah. Maybe what we want --
ALEKSANDRA: But I think it complains about the question marks. You can add question marks so that it defaults to undefined and it won't break like trying to access a property on an undefined object.
JASON: Like these?
ALEKSANDRA: Yeah. This also and when we have the code. So, for example, like the playlist in the line 69 can be undefined. So, we also want a question mark there.
JASON: Here --
ALEKSANDRA: And also on the items.
JASON: Okay. So, here not so bad. The view will need to be async. Do I have to awaitAll when I make this async?
ALEKSANDRA: Do you... yep. You're gonna have to. So, yeah. I was thinking about it. Like this is going to be pretty complex. So, maybe we can extract it to a function?
JASON: Yeah. Yeah. Let's do that. Right. So, this piece here -- this all comes out. And we'll put it up here.
JASON: Say async function, get artist details -- I guess we'll -- getPlaylistDetails. And that will be the playlist. And these. So, then... we want -- good. You are gonna be a playlist. And call function. And then, you're still unhappy. Item.--
JASON: Artists. Type unknown. Probably there is something missing in the schema regarding the types. And TypeScript doesn't know what is the type of the artist.
ALEKSANDRA: So --
JASON: And this is where I would just start doing...
ALEKSANDRA: As --
JASON: Yeah, this is where I would start doing just a check. Like if item.track.artists.length is less than 1, then like just continue. I guess --
ALEKSANDRA: Yeah. But it's still gonna comply -- complain that artist is of type unknown. Because it might not be typed correctly in the -- it might be policing in the Spotify schema. So --
JASON: Maybe we just force it. Like item.track.artists as...
ALEKSANDRA: Yeah. And now there is a question. Maybe we can actually go to the -- to this -- yeah. I don't like it. Sorry. We can go to this schema and see why it doesn't work. Yeah. So, maybe we can find the play -- yeah.
JASON: The playlist.
ALEKSANDRA: Yeah. And then we have parameters. And then responses. We -- yeah. It returns something like one playlist. So, we have to find it.
JASON: Okay. Here?
ALEKSANDRA: Okay. Yes. And then now we have to find the playlist object. Okay. Not this one... oh, yeah. This one. And what do we have here? We have description, we have like basically everything that we saw before when we were displaying the playlist. Yeah. And we have tracks.
JASON: So, that gives us a pagingPlaylistTrackObject.
JASON: And this is circular?
ALEKSANDRA: Yeah. This is why we're having the issue. Probably someone didn't like add it correctly to the specification. And then we don't a actually know what the playlist track type. That's why feTS can infer it and defaults to unknown because, well, we don't know.
JASON: Got it. Track. Gets a track object. This is a rabbit hole, isn't it?
ALEKSANDRA: Yeah, yeah. So, this is why like working with that is not like particularly pleasant. If you were to just look at this constantly to figure out how to use the API --
ALEKSANDRA: That would be a lot of work, right?
JASON: Yeah. I mean, honestly, this is the kind of stuff that usually keeps me from dealing with it is I just make it -- I like just I give up. My brain just shuts down. Like any. It's fine. So, artist is an array of the artist object.
JASON: So, the artist object, then...
ALEKSANDRA: Oh, yeah.
JASON: Is here.
ALEKSANDRA: Here it is.
JASON: And it should have all of these -- so, it's not unknown. So, something is... hm. So, I might be like breaking the reference here.
ALEKSANDRA: Or maybe... oh, maybe -- maybe there is an issue that feTS is losing this type at some point. But that's okay. We can work with that. So, basically like let's say that we want to make sure that this item, this track, this artist is an array. Sob we can use array is array.
JASON: Okay. So, we want to do like array is array, item.track.artists.
JASON: I guess we probably want to do that cumulative. We can return false. And then -- oh. It hates all of this. I can't return false because I said that it was gonna get back a playlist thing.
JASON: Also, this is mad.
ALEKSANDRA: Oh, yes, so, here --
JASON: Oh, jeez.
ALEKSANDRA: Yeah. We also have to put the question mark.
JASON: Okay. So, that's fine. Okay. So, that part's fine.
ALEKSANDRA: So, maybe if tracks...
JASON: And then we can also... so, we get our artist. And then we could do like the same check here. Let's say artist.images. Will you leave me alone? Yes. I have defeated you, TypeScript.
JASON: Okay. So, then that should give us all of these. And we can return...
ALEKSANDRA: The filtered data. Yep.
JASON: Okay. So, this is our get playlist details. That... will be called here.
JASON: So, we can just getPlaylistDetails. This should give us back our filtered playlist which gets set here, which is unhappy because that is the wrong --
ALEKSANDRA: Not a playlist anymore. We have a different type.
JASON: This up to -- where is our other type?
ALEKSANDRA: Here, this.
JASON: Okay. And this is instead going to be playlist data which theoretically works? No. Type tracks --
ALEKSANDRA: Yeah, I think this is because in tracks we returned something else. I think we returned this false.
JASON: Oh, you're right, you're right. So, then we need to filter in...
ALEKSANDRA: Yeah. I don't think so it will narrow properly because it will like -- it will filter at the runtime, but it narrows at the TypeScript level. Maybe let's return undefined instead of false.
ALEKSANDRA: And then we can also say that tracks can be -- like the track or undefined in our type.
JASON: That looks like we are allowing that. And now it's happy again.
JASON: Now it's unhappy again.
ALEKSANDRA: Right now we are allowing that the whole tracks can be undefined. And we want to say that tracks can be either an object or undefined.
JASON: It is... yes. That is -- that is what is happening. And also, this is returning promises which means I need to get this whole chunk out of here. And we're going to, say: FilteredTracks, await. And then we will put the filteredTracks in here. Okay. Happy now? You are happy now. Okay.
JASON: Also good to lose a wrestling match with TypeScript. What are you mad about now?
ALEKSANDRA: So --
ALEKSANDRA: Yeah. The thing is -- I think it is the same thing that you mentioned before. That we now have -- oh, yeah. So, the track will be either undefined or the data. And actually, as someone is pointing in the comments. It's an array of promises. So...
JASON: Oh, oh, oh, yeah. Okay. That's -- so, I was on the right track and I just forgot to, like, do the rest of it.
ALEKSANDRA: Now we get a bunch for it.
JASON: Overload match this is call. This is a... why doesn't that work? Now what am I doing?
ALEKSANDRA: No, I think -- I think you have to add the array earlier. Like even earlier, I think.
JASON: Right here?
JASON: Oh, boy. We...
ALEKSANDRA: So, we have the playlist. And we are mapping...
JASON: Playlist, tracks, items. Term, undefined. How about that?
ALEKSANDRA: Yep. And now we also don't want to return undefined like inside of this promise. So, if we add the or empty array after the map. So, if you scroll --
JASON: So, this is my after the map.
ALEKSANDRA: Yeah. And now we also have this undefined problem.
JASON: You can't be...
ALEKSANDRA: Yeah. Because we only...
JASON: There we go.
ALEKSANDRA: It's fine.
JASON: Sheesh. Just leave me alone, TypeScript. I'm trying to write code. And now you're unhappy because... undefined is not --
ALEKSANDRA: Is not -- yes.
JASON: Do I need to mark this as... let's see... you're going to return playlist data. That should line it up?
ALEKSANDRA: Yeah. And then you're -- after you added that, this -- this type, you can see that this return filtered is highlighted because... yeah. We return undefined here. So, I would -- like instead of adding this -- this playlist data as the return type, we can let TypeScript infer that itself. And inside of the useEffect we can say that with the data came undefined, then just skip or something like to. So, like if the -- basically, if the playlist is undefined, then do whatever. Yep.
JASON: Will you please let me play... why?
ALEKSANDRA: Tracks are incompatible because yeah. Because we have the array of track or undefined. So, we can modify the type slightly. It's not an issue. So, we can -- let's wrap it in params. The tracks object.
JASON: Like this?
ALEKSANDRA: Before the -- before the square brackets. So, just the -- just the object.
ALEKSANDRA: Yeah. And let's -- no. Like the -- the regular params. Like the, you know, like you have the function. Oh, yeah. And then after the -- the object bracket, let's do "Or undefined." But we do -- yeah. Like the single one. Because we are in the type already. Yep.
JASON: K. Okay. Please, please work. Okay. So, then we have a simplified. Ah-ha! We do it!
ALEKSANDRA: Yes. Took us a while. But we did it.
JASON: I still think it was less time than if we had to tried to write this from scratch.
JASON: We can do one of these, and then href, playlist.link, and then listen on Spotify. Right. Okay. So, we're getting close. And then to -- we'll leave out the iFrame, that actually -- we didn't see that link.
JASON: And then we can set up a -- this. And --
ALEKSANDRA: And the list of the tracks.
JASON: Yeah. So, we'll do playlist.tracks.map. And we get our track. And... we will return this. And we're in React so we need a key. The key can be the link. That should be unique. And then we'll have the -- what do we want to do? We can do --
ALEKSANDRA: Oh, yeah. Also, the track can be undefined. Yeah. So, if there's no track, we can just skip. Yep.
JASON: Okay. So, we'll say, track.link. And track.name by artist.
JASON: Okay. And why don't we put the image of that. So, we'll do image source, track.image. And the alt will be... track.artist.
JASON: All right. There we go. Okay. So, then we can do the tiniest --
ALEKSANDRA: Maybe we can bring back the CSS file.
ALEKSANDRA: It will fix itself.
JASON: I think let's do -- let's do the ul at display: Flex. And then we'll do li as width like 150-ish. Is to be display: Block. And width: 100%. And if I reimport this...
ALEKSANDRA: I think it worked out of the box because it's also imported in this main TSX.
ALEKSANDRA: I think that's the correct entry point in Vite.
JASON: Okay. Got it.
ALEKSANDRA: And then... yeah. I guess...
JASON: Up. That was weird. That was weird -- it was like -- that was like it caught it in a weird way. And then decided that like actually just kidding, I didn't want you to do that. Or I broke it?
ALEKSANDRA: I think...
JASON: That's not correct.
ALEKSANDRA: I think that's because it's very narrowed? And like if we wrap, then it's gonna take like the whole space, like each image. Maybe we can set a max width for the images.
JASON: Yeah. You are right. That should be wrap... the images. These -- oh. The -- my --
ALEKSANDRA: Oh, yeah.
JASON: My size I screwed up on. Okay. That makes sense.
ALEKSANDRA: Yeah, we'll need to work on accessibility. But it's better...
JASON: Then we can set a gap of like 1rem. A list style of none. And zero, and then we make this an actual size. And I mean, this is -- like it's not -- this is not perfect. And the colors are definitely a little whack-a-doo, but this is okay.
JASON: And now we've got kind of like a playlist thing. Which is -- which is pretty cool. And we built that, you know, I would say we spent more time wrestling with TypeScript, which I feel like is gonna be a universal case.
ALEKSANDRA: Yeah, I think we wrote all the related code in like 5 minutes and then we spend the rest figuring things out here.
JASON: Yeah. And this is -- this is just like -- this is very... it's very like a breath of fresh air to be able to kind of take the thinking about data out of the way and just be able to pull these things in. Like it just -- it just feels nice, right?
JASON: And so, going back through this, just to recap since I think we did a lot of like mental gymnastics around making TypeScript stop complaining. We were able to set up this client. So, we've got our feTS client which just grabs the Spotify Open API Spec. We specify the endpoint. And then we create this client. And then we had did issue by importing this client and a couple helper types, we can fetch all of our artist data. All of this was autocompleted for us. We were able to get our playlists, all of this was autocompleted for us. And even with the pretty custom like trimming down this data, we still -- we end up because we did the hurdle jumping of figuring out how to get these to stop yelling at us, we still get autocomplete all the way down. Like that is just, ooo, that is good. That is really good.
ALEKSANDRA: Yeah. We started this stream by mentioning tRPC. And I feel like -- like what tRPC is doing really well is like you don't have to think about this data. You don't have to think about fetching, about this API layer. You just can like ship your code. And I feel like there was something missing like that in the world of REST. So, here we go.
JASON: Yeah. I think this is -- this is really good stuff. So, for somebody who wants to keep going on this and learn more, where -- where should they go to keep learning?
ALEKSANDRA: So, they should definitely see the documentation. So, yeah. If you can open it. Yeah. Here is the whole documentation for the client. Like there are a bunch of things we didn't cover. Like there are plugins, there are middlewares that you can also use. There are some other like helper utilities like we used this OAS output. So, there are also like some utilities to get like the models from the Open API schema. And so on. So, I think this is a -- a good starting point. And like it's obviously open source. So, if someone has like any -- any issues with using it or suggestions how it can be improved, then definitely they can reach out on GitHub, create an issue, or maybe even directly to me or to Arda. And yeah. So, here is the repository. And we've discussed a bunch about the client. But also if someone is interested about like in building REST APIs in TypeScript, then I definitely recommend checking out the documentation for the server. So, it's under the same URL. Yeah. We have the server part.
JASON: Very, very cool. And you can always reach out to Aleksandra directly.
JASON: On Twitter.
ALEKSANDRA: My DMs are open.
JASON: And we figured out the part where DM -- there was the bug where DMs got like half-closed on Twitter.
JASON: I think we've all figured that out at this point.
ALEKSANDRA: Yeah, yeah. I saw it worth mentioning that. I turned it on or off. I turned my direct messages on again. Now everybody can reach out to me.
JASON: Well, this was so much fun. I really appreciate you taking the time. This was -- this was a blast. And folks, make sure you go and check out all this stuff. Just go give it a try. We have a lot of great stuff coming up on the show. So, first and foremost, this episode, like every episode was live captioned. We've had Amanda here with us all day from White Coat Captioning White. Thank you so much. Made possible by Netlify and Vets Who Code to sponsor the show to keep it accessible and for these great things to happen. When you're checking out things, make sure to check out the schedule. We have Jess Sachs next week, talking about Faker.JS. I'm very, very excited about that. I've booked, Una is coming on, Adam Argyle is coming on the show, I was talking to Addy Osmani, mark your calendars and we would continue to hang out. I would love that. Aleksandra, anything you want to say before we wrap this up?
ALEKSANDRA: Well, I want to all again to like I recommend checking out feTS. And then reaching out to me to tell me what you think about it. And also thank you very much, Jason, for having me here. As I mentioned before, it was my dream to come and join you on this livestream. So, I'm really happy that we get to talk about feTS and build something fun.
JASON: It was an absolute blast. And thank you for coaching me through all of those TypeScript gymnastics. Thanks for hanging out. We're going to find somebody to raid and we will see you all next time.
Closed captioning and more are made possible by our sponsors: