There are amazing tools out there providing you with excellent type safety. But when you get to the client-side fetching, things go wild. Even if you have perfectly typed backed, you lose the type information during the client-side communication. Yes, you can use GraphQL or protobuf and generate types, but... what if I told you there's an easier way? A way that lets you develop your apps smoother than with REST or GraphQL? How? RPC! Say hi to maximum productivity with fantastic developer experience.
Statically Detecting React App Bugs with TypeScript and ESLint
AI Generated Video Summary
1. Introduction to Static Analysis
And honestly, React Debra Lynn, I've got some anger in me. I've got a lot of problems with the tech industry, and now you're going to hear about it. As software developers, we've mostly become accustomed to the fact that many of our friends and family are never going to understand what we do. We don't like it. We disagree with their unwillingness to learn, but some people just cannot or will not be able to understand computers or even learning to code. Here's an actual quote from my fantastic loving parents, we don't know what TypeScript code is, but we're very proud of Josh and are sure it will be a lovely book. Referring, of course, to Learning TypeScript, available on Amazon O'Reilly and other retailers, buy it today.
Okay. Coding can be falsely intimidating, the issue is not that the concept of using a computer is unusually difficult on its own, many people can figure that out, it's that it is incorrectly assumed to be difficult by many folks. Many people who aren't tech capable find it hard to imagine using a computer at all, even, God forbid, even learning to code, and so are intimidated and distracted and discouraged from doing it. A big source of the problem is that a lot of folks grew up accustomed to the idea that computers are difficult, impossible to understand contraptions. And then they have a bunch of people yelling at them, oh, send a file, open Facebook, attach an attachment, press start, whatever, that's hard. What on earth? And the problem is made worse because many developers, because many people, do not explain technical or complex subjects well. It's something a lot of us struggle with. And a non-technical person can only go through so many bad explanations, convoluted descriptions, before they decide the problem is me, I'm just never going to understand this.
And let that be context for you, as I talk about two problems, changing context and bad explanations, for static analysis, tooling that analyzes your code without running it. Because in parallel, many software developers experience that very same false intimidation, even though they've already learned how to code, they know how to use computers, when they try to think about static analysis. Static analysis tooling for many years was thought by many to be just in the purview of academics or hardcore language engineers. And when they tried to look at resources for static analysis, what they got was high-level theoretical explanations, academic shenanigans, things that are theoretical rather than introductory and practical. But that's not the case. Maybe it never was. It certainly isn't now. Static analysis does not have to be intimidating. The static analysis landscape today, especially for modern TypeScript codebases, I promise you is much more approachable than it's ever been. I think you will find that the concepts I'm going to walk you through today are very relevant and applicable for your projects, and you may even enjoy using them when you get back to work on them. So this is static analysis is awesome, a.k.a.
2. Detecting Bugs with TypeScript and ESLint
Combining ESLint and TypeScript allows for detecting application defects and bugs through actual bugs in your code. TypeScript understands the meaning of your code and provides APIs for more informed linting decisions. We'll walk through a component example to explore potential pitfalls and the importance of type checking with TypeScript. Using the TypeScript ESLint plugin, we'll parse our project and make object types available to ESLint rules. Extend the recommended rules from both ESLint core and TypeScript ESLint for better results.
statically detecting React app bugs with TypeScript and ESLint. With me, Josh Goldberg. I'm going to show you how combining ESLint and TypeScript allows for detecting whole swaths of application defects and bugs, such as unused code or unnecessarily async code, through actual bugs in your code, that you will find before your app mysteriously breaks only for some users some of the time.
Let's do this. First, some context setting for the tools themselves, TypeScript and ESLint. Why would you want to level up ESLint rules with TypeScript in the first place? ESLint sees only the raw syntax of your code. It can check for rudimentary issues, such as unused variable, naming convention violations, and so on, but it doesn't really understand what that code means. TypeScript understands what that code means. It does a deep dive into your code, it understands your types, it understands what everything is meant to be, and then has APIs that we can use in our lint rules to make much more informed decisions based on those types.
Today I'm going to walk you through some components that look a lot like this my button example, and we're going to play one of my favorite programming games, Is This Safe? This component is meant to take in give or take two props, I've hid anything unrelated. Action and children, this action in particular is relevant and interesting, it's supposed to be a function call between two console logs. At first glance this code seems safe, it's fine, we take in two props, we render children, we pass this on click call back to a button, it's all fine. But without TypeScript's deep knowledge of the type system and this code, we have no way of linting about a whole bunch of plethora of bad potential pitfalls. So we're confused. Maybe async actions aren't allowed in this app. Maybe action isn't always provided. If it's a function in the first place, which we don't know, maybe it's supposed to take args. Maybe we actually want to explicitly handle rejected promises. There's all sorts of stuff we can't know without understanding what the actual type of our object is. What the types in our code are. So now we're going to go through some type checking. We're going to use TypeScript to parse our full project to understand potential object types. And then make those types available to ESLint rules using the TypeScript ESLint plugin. Now, if you have set up ESLint on TypeScript code before, it's likely you've done something like this. You've installed the TypeScript ESLint parser package, which is the parser that allows ESLint and fun fact, also Prettier, to read in TypeScript syntax from your files. And maybe you've also installed TypeScript ESLint ESLint plugin. The package that contains a whole bunch of Lint rules tailored to TypeScript code. You may have been set in your ESLint config, the parser and plugin settings. If this is all new to you, fun fact, that's how you do it. But there's another step I recommend many codebases take, which is extend the recommended rules from both ESLint core and TypeScript ESLint.
3. Exploring TypeScript ESLint Rules and Code Sample
For most projects, it's recommended to use the standard rules from both ESLint core and TypeScript ESLint. However, the recommended TypeScript ESLint rules do not include rules that require type information, as they can slow down the linting process. To enable these rules, you can extend the recommended rule set, configure the parser options, and specify your tsconfig file. Now, let's move on to the code. We'll be looking at a demo application called TypeScript ESLint React demo, which is a simple create-next-app application. It has a page with buttons that increase a call count and change the page background color. The first rule we'll explore is awaitAvailable, which checks if the await keyword is used on a value that is not available. Now, let's take a look at the code sample in our homepage, where we have an example action function that sets the color state and increments the call count. This function is used by our await thenable components.
All right, we're done. Four and a half project settings in, and we are ready to look at some code. The demo we're going to go through today is open source. You don't have to clone it to attend this talk, but if you want to play along, you can. GitHub.com slash Josh Roke-Goldberg slash TypeScript ESlint React demo. It's also TypeScriptESlintReactDemo.Vercel.app. Love my Vercel employees. And it is a rather rudimentary create next app application. It's just got one page. We've got a call count state and a custom color state, defaulting to zero and undefined, nothing, respectively. On this page, we have some buttons. When I click a button, I increase the call counts and set a custom color to a random color, which then also changes the page background. Great.
4. ESLint and TypeScript Code Insights
The button should be disabled while the action is running, but it's not. The ESLint rule complains about an unexpected await of a non-promise non-ventable value. Removing the awaits doesn't affect the code, and we can remove the unnecessary state and simplify the code to one line. ESLint powered by TypeScript provides deep insights into code defects.
And it's got this little button disabled equals running referring to that piece of state so that the button should be disabled while the action is running. For async code, this is totally reasonable, but if we go back to the demo, we never see that the button is disabled. So this may or may not be considered a bug. Some would just call it dead code, but either way, it's a problem that we should fix in our app.
And actually, we're getting a complaint from our ESLint rule here. We've got ESLint extension running in VS Code. Unexpected await of a non-promise non-ventable value. It's complaining that my action is typed as a function that returns void, meaning we ignore what it returns. We don't care about it. But I'm awaiting it, which is wrong, because it's not a promise. It's not available. So if I were to remove the awaits, the code should still run, and if I go to the demo, refresh even. Yep, still looks good. Because that await did nothing.
In fact, if I actually use the little extension popover thingie here, I can quick fix, that the rule tells us, to remove the awaits. And then async our function on click has no await expression gets complained to me from a separate ESLint rule, powered by TypeScript, our require rate rule, letting me know that this function doesn't even need to be async in the first place, because there's no await call on it. And I can even take this one large step further by noticing that I am setting state to true, calling a function synchronously, and then setting the state to false. So this state isn't even necessary in the first place. I can totally get rid of my use states. It's always going to be false, which means the button disabled equals false doesn't even need to be there. My on click is a function that just calls action. So I can just simplify the code to be one line inside the function. Boom. Amazing. Love it. Just to be sure, going back to the demo. Yep, everything here looks good. Awesome. That is one example of three where ESLint powered by TypeScript can inform us of some surprisingly deep insights into our code about code defects. Yay.
5. Exploring Floating Promises and Misused Promises
The next rule I want to look at is no floating promises. A floating promise is one that is created without any code set up to handle any errors. Floating promises can cause issues such as improperly synchronized operations and ignored promise rejections. In the demo, we have a button that is supposed to wait before performing an action, but it doesn't get disabled when clicked multiple times. This is because we forgot to add the 'await' keyword before the action. By enabling the 'no floating promises' rule, we get a complaint that promises must be awaited. Adding the 'await' keyword fixes the issue. The last rule I want to show is 'no misused promises', which forbids providing promises to logical locations where they are not handled properly. In the demo, we pass an async function to a button's 'onClick' attribute, causing a complaint because React doesn't handle async functions. By intentionally throwing an error in the async function, we demonstrate that the error cannot be handled by the framework.
But let's move on. The next rule I want to look at is no floating promises. My personal favorite ESLint rule. From the docs page, you will notice this is a pattern. All of our rules are under TypeScript. A floating promise is one that is created without any code set up to handle any errors in my throw. Floating promises can cause several issues such as improperly synchronized operations, ignored promise rejections, and more.
We will see both of those things in this demo. First, I want to show off a direct violation here this button which is slightly more advanced than the previous. It is supposed to wait a second before doing things. And although it works fine if we just click it once, if we click it a bunch the demo is not broken the way it's supposed to be. If we click it a bunch of times we see that it doesn't actually get disabled. What's happening here? Well, we in this no floating promises version call to this example action async which waits a second before calling that same example action and we forgot to add the await in front of the action. Notice how in this one it's typed as a function that returns promise void. Pardon me. So, our code doesn't actually await the function before then setting the states. So, we get this nice little disco effect if we click it a bunch of times. And if we enable the no floating promises rule I just disable that in the config so that we wouldn't get a squiggly immediately in the other files. We get this nice little complaint promises must be list of things you could do such as awaited you must await them. So, I'm going to add in an await here which means now we do await the action before set running false is called and yep our code is fixed as it was before. Yay!
Now, no floating promises is honestly my favorite rule because it can catch a lot of really intense stuff but it's very similar in theory to the last of the three rules I want to show you no misused promises. This rule forbids providing promises to logical locations such as if statements in places where the TypeScript compiler allows them but they are not handled properly. Floating promises are those that are created and then not handled at all, nothing uses them. Misused promises are when you do handle a promise technically you provide it to somewhere but that somewhere doesn't have any logic for handling promises. And to illustrate the difference I will show you the last demo which is our no misused promises component. Here, we are passing this on click to the button, same as we were before. And notice how we got the little squiggly here, the complain, promise returning function provided to attribute or avoid return was expected, because the on click and on whatever events, on DOM elements in frameworks such as React don't handle async functions. They don't handle the idea that the function might take more than constant synchronous time to run. And in this function, I've done something evil. I said that if call count is greater than two and math.random is greater than .5, we're going to throw an error, gotcha, which means that because this asynchronous code is rejecting, we're not going to be able to handle it in our framework.
6. Handling Exceptions and Resources
If you encounter a complaint or failure in your code, wrapping everything in a try-catch block may seem like a reasonable solution. However, there are potential exceptions that can still occur, even with try-catch. To safely handle async functions, it is recommended to create a unified handle using a function like 'runSafely', which takes in an action, a function that creates a promise, and a synchronous onError handler. By implementing this approach, you can ensure your application remains safe and doesn't crash, even when users explore edge cases. These are three fantastic ESLint rules from the TypeScript ESLint plug-in that I highly recommend enabling. For more resources, ESLint.org offers great documentation and a user-friendly UI. You can also check out the TypeScript ESLint React demo for a demonstration of these concepts. If you would like to support my work, you can buy my book 'Learning TypeScript' or sponsor me on GitHub. Additionally, the team behind TypeScript ESLint has an open collective and would greatly appreciate your support. Thank you for watching and enjoy the rest of the conference!
If we get a complaint, if we have a failure, haha, there we go. It's just going to go to the root on error listener. Our code isn't going to know what to do with it. Fun fact. This is a very similar popup style in a lot of modern dev tooling such as create react app and create next app where you get this, I think informed by web pack, little complaint.
Now, the way to do this you might at first have an instinct 4 would be to wrap everything in a try catch which is totally reasonable. Oh, no, error. And then maybe also running false there. Which would fix the error if we refresh, go a couple more times and then finally get the complaints. Come on, randomization. You're killing me here. Come on. Oh, lordy. Well, great. Looks like it never broke. I bet if we check the console, yup, got a few oh, no's. But this actually still might be a problem because what happens if you do something dangerous in your catch block, which accidentally has a bug, lol. And then you run it again, and you again are in the case where even though you try catch, you still have a potential exception in your code. So, the actual only way to really safely do this is to make some sort of unified handle for your async functions. I would highly recommend if your application is meant to be strongly safe and that it doesn't crash, even for users who are exploring edge cases, if you really care about your users not experiencing crashes, do some sort of function like this run safely function, which takes in an action, a function that creates a promise, and a synchronous on error, then calls action.catch on error, so that you can create onClicks or other event handlers that are a run safely of whatever your async code is and then something that handles the error. In this case, just for demo purposes, I've set up an error state, which gets shown to the user in a span with ARIA live assertive. So now, if we go to our demo and click, this may fail safely. If it fails, we do still get the gotcha, and then the button becomes undisabled or reenabled, even though the code had a failure. Awesome.
So, those are three fantastic ESLint rules from the TypeScript ESLint plug-in I would highly recommend enabling. And that's all the demo stuff I wanted to show you. For some resources, ESLint.org is fantastic, they just did a big site overhaul with great documentation rewrites and lovely new UI. We had a slightly less impressive site redesign over at TypeScript. And if you want to see the demo app, again, it's linked on the side, TypeScript ESLint React demo. If you want to support me in doing work on making all this better for you, you can buy my book Learning Typescript. And please sponsor me on GitHub, github.com/.sponsors/.JoshuaKGoldberg. And if you want to support us as the team behind TypeScript ESLint, we do have an open collective, opencollective.com/.typescripteslint. We could really use your support because we're very underfunded and very understaffed right now, and we have a lot of features, performance issues, and bugs that we'd like to tackle this coming year. All that being said, thank you very much for watching. Thank you to the conference organizers for setting all this up. You can reach me on Twitter, Joshua K. Goldberg, and I hope you enjoy the rest of the conference.