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
Transcription
Hey everyone, I'm Josh Goldberg. I'm an independent open source maintainer focusing on static analysis tooling around javascript and typescript. Most notably, I work on typescript ESLint, the tooling that allows you to run ESLint on your typescript code. I'm also the author of the Learning typescript book recently published through O'Reilly. 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 types 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. Learning 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, attachments, press start, whatever. That's hard. What on earth? 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, aka 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. We can check for rudimentary issues such as unused variables, 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 in children. This action in particular is relevant and interesting. It's supposed to be a function called 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 callback 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 FunFact, 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 then set in your ESLint config these parser and plugin settings. If this is all new to you, FunFact, that's how you do it. But there's another step I'd recommend many codebases take, which is to extend the recommended rules from both ESLint core and typescript ESLint. For most projects, you generally want the standard good recommended rules from both of them. ESLint core enables stuff available and informative for all javascript, whereas typescript ESLint rules are tailored to typescript code. And these are great pieces of information. We recommend using them. But one caveat, we don't include rules that require type information in the recommended typescript ESLint rule sets, because asking typescript for type information does make your lint take a little longer and requires a little bit extra config. So if you want to enable those rules, you can also extend the recommended requiring type checking rule set config and also add in a parser options dot project setting to your config, pointing to your TS config. This allows our plugin rules to know that that's the TS config to use for type information, to know your typescript compiler options. 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 typescript ESLint react demo dot Vercel dot app. Love my Vercel deploys. 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. The first rule I'm going to look through is awaitAvailable. From typescript ESLint dot IO slash rules slash awaitAvailable, the docs page for this rule, if the await keyword is used on a value that is not available, the value is directly resolved immediately. While doing so is valid javascript, it is often a programmer error. Fun fact, available are most commonly used as promises. Available is a description of the type promises the actual class. I'm going to show you the code sample here. In our, oops, got the extra demo there. There we go. In our home page, pardon me, we have this function here, this example action, which creates a color variable equal to a random color, increments a call count state that's set up here by one, sets the color as the custom color state, and returns that color. This function is then used by our await thenable components. Now await thenable takes in action and children, much like that my button example, and it sets a piece of state running to true or false before and after action. 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-thenable 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 a thenable. 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 thingy 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 state. 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 actions, so I can just simplify the code to be one line inside the function. Boom. It's async. Love it. And 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. 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.io. 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're going to see both of those things in this demo. I want to show off a direct violation here, this button, which is slightly more advanced than the previous. It's 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 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 disabled 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 provided 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 where we were before. And notice how we got a little squiggly here, the complaint, promise returning function provided to attribute where 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. If we get a complaint, if we have a failure, ha-ha, 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 for would be to wrap everything in a try catch, which is totally reasonable. Oh, no, error. And then maybe also start running false there. Which would fix the error if we refresh, go a couple more times, and then finally get the complaint. 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, yep, got a few oh-noes. 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? Blah. 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 in 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 on clicks or other events, 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 plugin 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 but still great site redesign over at typescript ESLint.io. And if you want to see the demo app, again, it's linked on the slide, 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 Through O'Reilly, and or please do sponsor me on GitHub, github.com slash sponsors slash Joshua K. Goldberg. And if you want to support us as the team behind typescript ESLint, we do have an open collective, opencollective.com slash typescript ESLint. 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. Bye. Thank you.