Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Defensive Programming and the Use of TypeScript

Ifeora OkechukwuFollowBetter Programming--1ListenShareUnlike languages such as Java, which have type safety that extends into the runtime, Typescript is relieved of its type safety duties at compile time. This means your code has a good number of guard rails and reasonable constraints at runtime but not enough. At runtime, it’s all just good old JavaScript written defensively… to an extent. Most of the time, the unexpected can happen, and TypeScript does a pretty good job of forcing you to handle those unlikely cases. In other words, TypeScript forces you to write defensively.Defensive programming is writing code that can handle unlikely scenarios at runtime. For example, if we expect the server to reply to client HTTP requests and apply some logic to the data, we get a response from the server. What if the server doesn’t respond with the data we are expecting but responds with something else, like an error? Will our code still recover and deal with this error without breaking or crashing on the client and therefore displaying an invalid user interface? These are the questions that defensive programming answers for us. Let’s take a more relatable example: Say we need to read a value stored in localStorage like so:The code above will work fine as long as a JWT exists inside localStorage for the storage key userToken. In the same vein, the code will fail otherwise (the fetch HTTP request will return an error because userToken will be null. So, how can we defensively safeguard against that? Well, we ensure that userToken is a JWT like so:The great thing about defensive programming is that it isn’t limited to any one programming language. It is also applicable to any environment where type safety is important. In fact, it is needed in dynamic languages like JavaScript as much as it is needed in static languages like Java.When using a programming language like Java, you have several options for dealing with exceptions, even the ones that are type-related, such as Unchecked Exceptions. You also have Errors (e.g., VirtualMachineErrors) which you cannot recover from at all at runtime. These Java errors are very much applicable to JavaScript as JavaScript does make use of a virtual machine (well, most modern JavaScript engines like V8 are full-fledged virtual machines and do throw VM errors like Heap Out Of Memory) in the same sense as Java does with the only difference being one is compiled fully into an intermediate representation (Java class files) and the other is executed as it’s being compiled — JIT compilation.Furthermore, the kinds of issues TypeScript helps you with are closely related to Unchecked Exceptions in Java. One very pertinent one is the NullPointerException. It is an Exception that occurs mostly at runtime when you reference or interact with a null value as if it were not one. There is no compile-time solution to avoiding or eradicating this Exception at runtime. The best you can hope for is that you are aware that it can occur in certain areas of a Java codebase based on certain heuristics. Therefore, you can only deal with the NullPointerException by writing your code defensively.When someone says they love working with TypeScript, what they really mean is they love the fact that their IDEs pick up on intelliSense delivered by static type inference (not auto-completion anyway — there’s a difference).TypeScript will benefit more as a first-class citizen inside of JavaScript engines. This means TypeScript is allowed to operate mostly in a dynamic context (runtime — no over-elaborate subtypes and no compilation), but that is much less likely to happen (for backward compatibility reasons mostly) or if TypeScript could infer mostly (I know TypeScript sometimes infers implicitly) without explicit type annotations in a static context (compile time — few elaborate subtypes and compilation) but again this is also less likely to happen (for marketing reasons, I guess).I am not quite sure the TC39 types annotation proposal won’t get to Stage 4. If it does, it might help augment JS doc comments, or maybe not (time will tell).Furthermore, while Deno says it treats TypeScript as a first-class citizen, it still doesn’t let it operate past compile time. However, the software developer is no longer saddled with the responsibility of compiling it manually, so Deno takes care of that. This is great for ergonomics and all, but it doesn’t go to the heart of the issue — Will the compiled TypeScript (now JavaScript) source have no type errors at runtime?TypeScript is a mature tool, and the benefits it provides are substantial for sufficiently mid-sized and/or large codebases. Everyone speaks about these benefits, but few people speak about the upfront cost and whether it is worth it. Recently, Rich Harris (creator of Svelte and Rollup) discontinued the use of TypeScript in building the Svelte library (meaning the Svelte library no longer uses TypeScript but still allows you to use TypeScript while building Svelte applications) in favour of JS doc comments. His reasons for this switch were due to the high cost/benefit penalty when using TypeScript to build libraries and frameworks.So, why is it that after all the upfront work of using actual types (not things like: any or unknown), it still fails to eradicate many type errors at runtime? Why is that? Well, for one, the quality of results from using TypeScript in eliminating type errors is largely dependent on the quality of type annotations supplied by TypeScript itself for certain APIs and how well you define your own custom types (and subtypes). How you define your type informs how much TypeScript forces you to write code defensively. There is an upfront cost to be paid when using TypeScript. Most times, you’d have to ask yourself whether that cost is worth it by doing a basic cost/benefit analysis. Usually, the less explicit types, the better! At other times, it is vital as the benefit outweighs the cost, especially when you have a very large JavaScript codebase, as I said earlier.You have to be vigilant for times when TypeScript drops the ball on type safety because TypeScript is not a full-proof solution.There are four cogent reasons why TypeScript is not enough and why you cannot solely rely on TypeScript to keep your codebase fully type-safe:Every other day, code framework builders, library makers, browser vendors, and, of course, the TC39 are deploying new releases of their software that come with features and brand new code APIs that the current in-use versions of TypeScript know nothing about. For instance, optional-chaining (TC39 ECMAScript feature) which was released by major browsers between February and March 2020, was already supported in TypeScript v3.7 (released on the 5th November, 2019). However, the new React Server Components (RSC) dropped earlier this year (2023), and since TypeScript v4.8 (released on the 25th August, 2022) didn’t have TypeScript support for more than five months! TypeScript v5.0+ fixes this now, so things are good again.Also, some browser (BOM) APIs have incomplete typing of the complete set of fields and/or properties they possess. For instance, when building Progressive Web Apps (PWAs), I like to use navigator.standalone to determine various PWA display modes and installation statuses and also to use the indexedDB queryresult for when the query results are accessed from the event target property (event.target.result).Turns out that for several TypeScript releases (until maybe recently — I can’t tell), the properties on these objects were not catered for. So, most times in the past, I have had to come up with a types/index.d.ts file that looks like this:www.typescriptlang.orgTake a look at this TypeScript snippet above, which is also here. You notice that the variable channel (defined using let) is suddenly undefined on line 12. The heuristics TypeScript applies are, at best, the worst-case assumptions in the static context (meaning TypeScript can’t really run the code as it can only analyse the code statically). This happens because the compiler cannot really be sure of the control flow dynamics of the code when the callback passed to channel.messages.map is executed at runtime so it makes a safe judgement call that channel might be undefined.This Stack Overflow answer delves more into this behaviour that the TypeScript compiler exhibits. In other words, TypeScript doesn’t and can’t always follow code logic as written by the programmer or software engineer because its’ ability to infer the type of a variable confidently is considerably impaired by control flow concerns. The error is, however, fixed when you define the channel variable using const instead of let.There are other cases where you can experience a very alternate behaviour in a more pragmatic context. Take a look at the code snippet below:It is important to point out that this alternate behaviour is not a bug but a feature of TypeScript. It is the thing that “forces” the programmer or software engineer to write code defensively. However, this alternate behaviour is not always consistent, as there are times when this behaviour breaks down completely and isn’t effective. For example, take a look at the snippet below:There are several problems with the code snippet above, which defines getResetCode() . The first obvious problem is that the parse variable on the line where we have the return statement is of the type any. How is that possible? Well, JSON.parse returns an object of type any and since it is assigned to the variable parsed then it, too, takes up that type. The second problem stems from the first one. The getResetCode() function isn’t type safe because JSON.parse isn’t type safe as well due to its’ default lax typing. The third problem is that the return type from the generic T doesn’t reflect the fact that the parsed variable could be null when returned from the function.A naive TypeScript programmer or software engineer may check out this code in the main branch of development because there are no squiggly red lines on the code editor for this function, and so they believe the code is type-safe. I mean, there are no variables in the getResetCode() function definition explicitly annotated as type any. So therefore, all must be well.Lastly, JSON.parse stands the risk of being passed a string that contains invalid JSON tokens. Like below:Are we handling this possible syntax error inside the getResetCode() function? Unfortunately, no!Thankfully, we can do a couple of things to clean up all these problems I just highlighted. We could modify the getResetCode function like this:As you can see above, the return type annotation is now T | null instead of just T. If we remove the return type annotation, the getResetCode() function returns a type of any. It turns out that JSON.parse which returns any is messing up our return type for this function definition. You can see other manifestations of this problem in this video. Recently, Matt Pocock of TypeScript fame released this npm package which solves this problem very well. Other solutions exist as well like this one. You should check them out!JSON.parse can return null or false or true when passed a stringified form of these three values respectively without throwing an error. Also, it can do the same for any primitive JavaScript data type value, which is also a valid JSON primitive value (token) types (e.g., numbers, booleans, null).Which is why I added the line in the try block as follows:Finally, in contrast to the example above, the correct (alternate) behaviour is exhibited when you try to make use of the error variable in catch block of a try/catch definition. The error variable can be anything and not just an Error object because JavaScript allows you to throw non-error “objects” such as strings or numbers or even null like so:So, this behaviour of JavaScript, although weird, is something that TypeScript has to accommodate, and in so doing, the default type assigned to the error “object” is any. Just like the return type of JSON.parseis any, so is the default value for the error “object” in the catch block. Kent C. Dodds explains this phenomenon quite well in his article.Finally, sometimes the default primitive and object types supplied by TypeScript aren’t enough. You may define a variable as a number type, but specifically, you want non-negative integers or negative float values. There isn’t any TypeScript type that (by default) provides this specificity and can validate the variable at runtime as meeting the criteria.JavaScript thrives or exists in predominantly two environments: client (web/desktop/mobile) and server. Also, wherever JavaScript can thrive, TypeScript can also thrive there too. But sometimes, the TypeScript type definitions/declarations of the same API common to the two environments, can differ, especially now that NodeJS is making space for some browser-based APIs on the server.For example, the atob() base64 decoding function that lives on the browser window is also available on NodeJS as of v16.0+. In any TypeScript file, the type definitions for the atob() is loaded twice (once for the NodeJS environment and once for the browser environment). Therefore, using it without explicitly indicating which environment you are using it from will result in the wrong type definition being used. So, instead of writing atob() , you have to be explicit by writing by referencing the Window object: window.atob() indicating you need the browser environment type definition loaded and used by TypeScript and your code editor.The same thing happens for setTimeout(). You have to be explicit about the environment you are writing code for by referencing the Window object: window.setTimeout() (this is an issue for isomorphic JavaScript and server-side rendering — especially back in the day) as opposed to writing setTimeout(). Also, when you need the timer ID returned from setTimeout() , it is tricky because the return type could vary depending on the environment. So, you have to do this like so:These differences sometimes get in the way of great type inference and sometimes safety, especially for software engineers who build reusable libraries and frameworks. What if the client-side JavaScript code using window.setTimeout()is now supposed to be reused in a React Native environment? How do we deal with it?It means we have to write the code defensively (otherwise called type narrowing) like so:Here are real-world examples of using defensive programming in popular open-source libraries (line numbers included for relevant code lines):As we write TypeScript code that gets the job done, there are places in our codebase where we use ES expression syntaxes that improve and enhance the developer experience at no cost to readability. An example of an ES expression syntax is destructuring. If you take a look at this project built by the talented, energetic, and fantastic full-time open-source software (OSS) developer: Sindre Sorhus.The project exists to extend the set of utility types (in addition to defaults like Pick, Extract, Omit, and Partial) available to TypeScript to aid software developers in defining types that make it a lot easier to work with TypeScript and ES6 syntax features and resolve errors much quickly. In this project, there is a pull request (PR) that was submitted recently (April 2023) that seeks to make it easy to deal with discriminated union types whenever they are destructured like so:As soon as you destructure (last line of code above), you get a TypeScript error: Property ‘error’ does not exist on ‘SuccessResponse.’Now, the only reasonable way to get rid of that error from TypeScript is not to destructure at all and then use type narrowing to proceed like so:This is one-way TypeScript takes away our ability to utilise ES6+ syntax options like destructuring. Unless we could define a custom utility type that can wrap around the union type and make it such that we can destructure safely without having to deal with silly TypeScript errors.Such utility type is the focus of this pull request and it will be an awesome addition should it be merged in.A lot of TypeScript developers simply define types, use them and think they are done.I got the line above from this very insightful article I read before writing this one. I kept nodding and agreeing with every point made in that article.As a TypeScript developer, you still have work to do after you define your types. You must deal with the very likely type of concerns that TypeScript may not be showing you upfront. You must write TypeScript defensively too.Fault tolerance is a real concept and applies to all kinds of software, especially software that deals with data it receives outside its static bounds. When making HTTP requests either from a client or from a server, you cannot be sure the server will always return data, so even with the types correctly defined, you still have to cater for faults where the server crashed and cannot return valid data by coding defensively.I have seen codebases where the TypeScript interface and object types are defined with optional fields, and optional chaining is abused to stupor. Function arguments are not validated for the possibility that they won’t match the type they are annotated with. Here’s one example:What I see most TypeScript developers do to fix the above is to start abusing optional chaining like so:If you want to learn more about where and when optional chaining can be used correctly, see this article. There is a way, however, to solve this defensively:Always setup a default value for any variable whose value is coming from outside your codebases’ total scope area (e.g., from a URL query parameter or URL hash or a server response payload or server response header)This will save you a ton of stress, and you don’t have to needlessly abuse optional chaining. Another way to solve this (overkill — if you are using it just in one place) will be to make use of the Maybe (Option) monad (Did you know that you can use monads with TypeScript and that a Promise is a Future monad? Story for another day!). Monads are value objects that wrap variables and can be very helpful in defensive programming and error handling. It can be used both in imperative language codebases as well as functional language codebases. The Maybe monad also takes away the need for if statements that have truthiness checks.The point is that you don’t need optional chaining here at all.When using or modifying any reference type variable within a more local or equally local scope than the scope the reference type variable was created in (especially if the value for the variable is from outside your codebase total scope area). Validate the type first before any direct use or mutation.Sometimes, when writing code, you get a variable defined outside a function definition that mutates it. TypeScript allows this to happen as long as the annotated or inference types align. However, what if the value that is used in the mutation isn’t hardcoded in the source file but comes from a server response like so:You notice above that the server responded with an array of strings with single digits, not numbers. Now, at runtime, there’s no enforcement from TypeScript, and since that value of newValue is dynamic (from a server response) and not static (hardcoded), scopedVariable is mutated in place upon the chnageScopedVariable() call. This makes scopedVariable an array of strings and not an array of numbers.To ensure that type errors don’t occur anywhere elsewhere, scopedVariable is used or interacted with, it is a safe defensive programming mechanism to validate the parameter type using assertion signature type guards before direct mutation.One thing you’d have noticed (from the code snippet above) is that I have modified the type annotation for the parameter of the changeScopedVariable() function from number[] to unknown[] to reflect the fact that newValue may or may not be an array of numbers at runtime and work things out from that standpoint.You can also use this validation to determine if parameters (whether optional or mandatory) for functions are of the correct type at runtime.Don’t abuse the use of try/catch blocks! Verify that an error is actually thrown by a line or lines of code before wrapping them in a try/catch block. The only error handling software needs is for errors that either have a high chance of occurring or will actually occur at runtime.There are times when you want to handle an exception or error. However, care must be taken not to overuse try/catch blocks, even though they are great tools for defensive programming. Read up on this article to find out more.One feature I would love to see in future versions of TypeScript that will further aid the type safety goals of TypeScript is the throws statement in Java. It is one of the best parts of Java and has a place in modern TypeScript. It will limit the abuse of try/catch blocks and further streamline the process of error handling in TypeScript and ensure that it’s consistent throughout the codebase. It will also significantly reduce the instances of “Unhandled Exception” at runtime. It turns out this was proposed in 2016 on the official GitHub TypeScript repo issue board but was explicitly denied. However, I urge the TypeScript team to revisit it soon.In this article of mine, I wrote down several tips on setting up better error handling and debugging and how important it is to handle errors closer to the entry point of any software program. One tip I left out mistakenly is ensuring consistent return types from all method and function calls.This is an example of the wrong ways people handle error scenarios in JavaScript by not ensuring consistent return types like so:There are a few problems with above code snippet. The first problem is that the tasks variable doesn’t have a default value. The second problem is error conditions are not raised explicitly. The third problem is that the getUserTasks function can return three different data types: boolean, undefined, and an array of tasks. It should return only one data type or at most two as a discriminated union like so (but we are still not out of the woods yet):The modifications made to the code snippet (above) are significant but we haven’t fixed the second problem we identified. Depending on how we choose to proceed, the second problem may not be a problem because we can always use an invariant to check whether or not the tasks array returned by getUserTasks() is empty and throw an error if it is. But how many software engineers in the real world make use of invariants ? Well, not many from where I am sitting. Also, not fixing this problem violates the good rule of thumb: crash early, crash often. Finally, it also improves code readability, comprehension and reduces cognitive load.So, we have to raise errors explicitly like so:A responsible use of TypeScript requires that additional guarantees are provided for and which will be utilised at runtime. The bare minimum of just defining and using types and subtypes isn’t enough.Furthermore, TypeScript configuration should be taken seriously when setting up TypeScript.Ensure you use TypeScript’s strictNullChecks compiler flag option to guarantee your code isn’t doing any illegal type conversions or implicit type coercions. This will catch a lot of errors that would otherwise be hard to track down.Also, ensure that the noPropertyAccessFromIndexSignature and noUncheckedIndexedAccess compiler flag options are also set for much better type safety with object literals and arrays in TypeScript.Finally, I am not in any way excluding the use of design by contract in building software systems that talk to one another. Defensive programming helps in situations where reality strikes a blow (e.g., service timeouts) and the contract is temporarily and/or extensively broken.Enjoy coding defensively!----1Better Programminga recovering perfectionist, unrepentant world-wide web addict, software engineer and gadget freak. Very involved in web applications of the future.Ifeora Okechukwu--Sergei SavvovinBetter Programming--8Dmitry KruglovinBetter Programming--31Ifeora Okechukwu--1Jari RoomerinBetter Humans--126Thomas SmithinThe Generator--118Love SharmainByteByteGo System Design Alliance--54Clive Thompson--9David Rodenas, Ph. D.--47The Coding DiariesinThe Coding Diaries--111HelpStatusWritersBlogCareersPrivacyTermsAboutText to speechTeams



This post first appeared on VedVyas Articles, please read the originial post: here

Share the post

Defensive Programming and the Use of TypeScript

×

Subscribe to Vedvyas Articles

Get updates delivered right to your inbox!

Thank you for your subscription

×