Home » Defensive Programming and the Use of TypeScript | by Ifeora Okechukwu | Jun, 2023

Defensive Programming and the Use of TypeScript | by Ifeora Okechukwu | Jun, 2023

by Icecream
0 comment

TypeScript is useful at scale, however after utilizing it for a number of years, it turns into evident that it’s not adequate by itself

Illustration by writer

Unlike languages comparable to Java, which have kind security that extends into the runtime, TypeScript is relieved of its kind security duties at compile time. This means your code has a great variety of guard rails and cheap constraints at runtime however not sufficient. At runtime, it’s all simply good outdated JavaScript written defensively… to an extent. Most of the time, the sudden can occur, and TypeScript does a fairly good job of forcing you to deal with these unlikely circumstances. In different phrases, TypeScript forces you to write defensively.

Defensive programming is writing code that may deal with unlikely eventualities at runtime. For instance, if we count on the server to answer to shopper HTTP requests and apply some logic to the info, we get a response from the server. What if the server doesn’t reply with the info we predict however responds with one thing else, like an error? Will our code nonetheless recuperate and take care of this error with out breaking or crashing on the shopper and due to this fact displaying an invalid person interface? These are the questions that defensive programming solutions for us. Let’s take a extra relatable instance: Say we have to learn a worth saved in localStorage like so:

const personToken = window.localStorage.getItem("personToken");

// Let's do one thing with `personToken`

fetch("http://localhost:3001/jwt-login", {
technique: "GET",
headers: {
"Content-Type": 'software/json',
"Accept": 'software/json',
"Authorization": `Bearer ${personToken}`
}
})
.then(resp => resp.json());

The code above will work positive so long as a JWT exists inside localStorage for the storage key personToken. In the identical vein, the code will fail in any other case (the fetch HTTP request will return an error as a result of personToken will probably be null. So, how can we defensively safeguard in opposition to that? Well, we make sure that personToken is a JWT like so:

const personToken = window.localStorage.getItem("personToken");

// Let's do one thing with `personToken`
// Check to ensure `personToken` isn't a null worth

if (personToken !== null) {
fetch("http://localhost:3002/jwt-login", {
technique: "GET",
headers: {
"Content-Type": 'software/json',
"Accept": 'software/json',
"Authorization": `Bearer ${personToken}`
}
})
.then(resp => resp.json());
}

The beauty of defensive programming is that it isn’t restricted to anyone programming language. It can also be relevant to any surroundings the place kind security is vital. In truth, it’s wanted in dynamic languages like JavaScript as a lot as it’s wanted in static languages like Java.

When utilizing a programming language like Java, you might have a number of choices for coping with exceptions, even those which can be type-related, comparable to Unchecked Exceptions. You even have Errors (e.g., VirtualMachineErrors) which you can not recuperate from in any respect at runtime. These Java errors are very a lot relevant to JavaScript as JavaScript does make use of a digital machine (properly, most trendy JavaScript engines like V8 are full-fledged digital machines and do throw VM errors like Heap Out Of Memory) in the identical sense as Java does with the one distinction being one is compiled absolutely into an intermediate illustration (Java class recordsdata) and the opposite is executed because it’s being compiled — JIT compilation.

Furthermore, the sorts of points TypeScript helps you with are carefully associated to Unchecked Exceptions in Java. One very pertinent one is the NullPointerException. It is an Exception that happens largely at runtime once you reference or work together with a null worth as if it weren’t one. There is not any compile-time resolution to avoiding or eradicating this Exception at runtime. The greatest you possibly can hope for is that you’re conscious that it could happen in sure areas of a Java codebase based mostly on sure heuristics. Therefore, you possibly can solely take care of the NullPointerException by writing your code defensively.

When somebody says they love working with TypeScript, what they actually imply is that they love the truth that their IDEs choose up on intelliSense delivered by static kind inference (not auto-completion anyway — there’s a distinction).

TypeScript will profit extra as a first-class citizen within JavaScript engines. This means TypeScript is allowed to function largely in a dynamic context (runtime — no over-elaborate subtypes and no compilation), however that’s a lot much less prone to occur (for backward compatibility causes largely) or if TypeScript may infer largely (I do know TypeScript typically infers implicitly) with out specific kind annotations in a static context (compile time — few elaborate subtypes and compilation) however once more that is additionally much less prone to occur (for advertising and marketing causes, I assume).

I’m not fairly positive the TC39 varieties annotation proposal received’t get to Stage 4. If it does, it would assist increase JS doc feedback, or perhaps not (time will inform).

Furthermore, whereas Deno says it treats TypeScript as a first-class citizen, it nonetheless doesn’t let it function previous compile time. However, the software program developer is not saddled with the duty of compiling it manually, so Deno takes care of that. This is nice for ergonomics and all, however it doesn’t go to the guts of the problem — Will the compiled TypeScript (now JavaScript) supply haven’t any kind errors at runtime?

TypeScript is a mature software, and the advantages it supplies are substantial for sufficiently mid-sized and/or giant codebases. Everyone speaks about these advantages, however few individuals converse concerning the upfront value and whether or not it’s value it. Recently, Rich Harris (creator of Svelte and Rollup) discontinued using TypeScript in constructing the Svelte library (that means the Svelte library not makes use of TypeScript however nonetheless lets you use TypeScript whereas constructing Svelte purposes) in favour of JS doc feedback. His causes for this change have been as a result of excessive value/profit penalty when utilizing TypeScript to construct libraries and frameworks.

So, why is it that after all of the upfront work of utilizing precise varieties (not issues like: any or unknown), it nonetheless fails to eradicate many kind errors at runtime? Why is that? Well, for one, the standard of outcomes from utilizing TypeScript in eliminating kind errors is essentially depending on the standard of kind annotations equipped by TypeScript itself for sure APIs and the way properly you outline your personal customized varieties (and subtypes). How you outline your kind informs how a lot TypeScript forces you to write down code defensively. There is an upfront value to be paid when utilizing TypeScript. Most instances, you’d must ask your self whether or not that value is value it by doing a primary value/profit evaluation. Usually, the much less specific varieties, the higher! At different instances, it’s critical because the profit outweighs the fee, particularly when you might have a really giant JavaScript codebase, as I mentioned earlier.

You must be vigilant for instances when TypeScript drops the ball on kind security as a result of TypeScript isn’t a full-proof resolution.

There are 4 cogent the explanation why TypeScript isn’t sufficient and why you can not solely depend on TypeScript to maintain your codebase absolutely type-safe:

1. TypeScript doesn’t at all times sustain

Every different day, code framework builders, library makers, browser distributors, and, after all, the TC39 are deploying new releases of their software program that include options and model new code APIs that the present in-use variations of TypeScript know nothing about. For occasion, optional-chaining (TC39 ECMAScript function) which was launched by main browsers between February and March 2020, was already supported in TypeScript v3.7 (launched on the fifth November, 2019). However, the brand new React Server Components (RSC) dropped earlier this 12 months (2023), and since TypeScript v4.8 (launched on the twenty fifth August, 2022) didn’t have TypeScript assist for greater than 5 months! TypeScript v5.0+ fixes this now, so issues are good once more.

Also, some browser (BOM) APIs have incomplete typing of the whole set of fields and/or properties they possess. For occasion, when constructing Progressive Web Apps (PWAs), I like to make use of navigator.standalone to find out numerous PWA show modes and set up statuses and in addition to make use of the indexedDB questionoutcome for when the question outcomes are accessed from the occasion goal property (occasion.goal.outcome).

Turns out that for a number of TypeScript releases (till perhaps just lately — I can’t inform), the properties on these objects weren’t catered for. So, most instances previously, I’ve needed to provide you with a varieties/index.d.ts file that appears like this:

declare class Stringified<T> extends String {
personal ___stringified: T
}

declare world {
interface IDBEventTarget extends EventTarget {
outcome: IDBDatabase;
}

interface IDBEvent extends IDBVersionChangeEvent {
goal: IDBEventTarget;
}

interface File extends Blob {
readonly lastModified: quantity;
readonly identify: string;
readonly webkitRelativePath: string;
}

interface Navigator {
readonly standalone?: boolean
}

interface Window {
JSON: null
parse(textual content: string, reviver?: (key: any, worth: any) => any): any

}
}

2. TypeScript doesn’t at all times comply with the code logic

let channels = [
{id: 1, messages: [1, 2, 3]},
{id: 2, messages: [4, 5]}
]

let channel = channels.discover(c => c.id === 1);

if (channel) {
console.log(channel.id);

channel.messages.map(message => {
console.log(channel.id);
})
}

Take a take a look at this TypeScript snippet above, which can also be right here. You discover that the variable channel (outlined utilizing let) is abruptly undefined on line 12. The heuristics TypeScript applies are, at greatest, the worst-case assumptions within the static context (that means TypeScript can’t actually run the code as it could solely analyse the code statically). This occurs as a result of the compiler can’t actually be certain of the management move dynamics of the code when the callback handed to channel.messages.map is executed at runtime so it makes a protected judgement name that channel is perhaps undefined.

This Stack Overflow reply delves extra into this behaviour that the TypeScript compiler displays. In different phrases, TypeScript doesn’t and might’t at all times comply with code logic as written by the programmer or software program engineer as a result of its’ capability to deduce the kind of a variable confidently is significantly impaired by management move considerations. The error is, nonetheless, mounted once you outline the channel variable utilizing const as a substitute of let.

There are different circumstances the place you possibly can expertise a really alternate behaviour in a extra pragmatic context. Take a take a look at the code snippet beneath:

const domElement = doc.getElementById("root");
const logInfo = window.localStorage.getItem("logInfo");

// is perhaps null as a result of the DOM node could or could not exists within the DOM
if (domElement !== null) {
console.log(domElement.nodeName);
}

// is perhaps null as a result of the storage could or could not include key "logInfo"
if (logInfo !== null) {
console.log(logInfo);
}

It is vital to level out that this alternate behaviour isn’t a bug however a function of TypeScript. It is the factor that “forces” the programmer or software program engineer to write down code defensively. However, this alternate behaviour isn’t at all times constant, as there are occasions when this behaviour breaks down fully and isn’t efficient. For instance, check out the snippet beneath:

window.localStorage.setItem(
"rc_219872992",
"0000000000000000000"
);

perform getResetCode<T extends Record<string, unknown>>(
resetId?: string | null
): T {

let parsed = null;

if (resetId) {
const encoded = window.localStorage.getItem(resetId);

if (encoded !== null) {
const decoded = window.atob(encoded);
parsed = window.JSON.parse(decoded);
}
}

return parsed;
}

There are a number of issues with the code snippet above, which defines getResetCode() . The first apparent downside is that the parse variable on the road the place now we have the return assertion is of the kind any. How is that attainable? Well, JSON.parse returns an object of kind any and since it’s assigned to the variable parsed then it, too, takes up that kind. The second downside stems from the primary one. The getResetCode() perform isn’t kind protected as a result of JSON.parse isn’t kind protected as properly on account of its’ default lax typing. The third downside is that the return kind from the generic T doesn’t replicate the truth that the parsed variable may very well be null when returned from the perform.

A naive TypeScript programmer or software program engineer could try this code in the principle department of growth as a result of there aren’t any squiggly purple strains on the code editor for this perform, and they also consider the code is type-safe. I imply, there aren’t any variables within the getResetCode() perform definition explicitly annotated as kind any. So due to this fact, all have to be properly.

Lastly, JSON.parse stands the danger of being handed a string that accommodates invalid JSON tokens. Like beneath:

JSON.parse perform blows up with a SyntaxError

Are we dealing with this attainable syntax error contained in the getResetCode() perform? Unfortunately, no!

Thankfully, we are able to do a few issues to wash up all these issues I simply highlighted. We may modify the getResetCode perform like this:

perform getResetCode<T extends Record<string, unknown>>(
resetId?: string | null
): T | null {
let parsed = null;

// Type narrowing
// @see: https://www.typescriptlang.org/docs/handbook/2/narrowing.html
if (!resetId) {
return parsed;
}

const encoded = window.localStorage.getItem(resetId);

// Local storage can return `null` if no worth was
// discovered for the `resetId` key in storage.
if (encoded !== null) {
let decoded = "";

// The string worth 'false' and string digits
// Will trigger `atob()` perform to throw a DOM
// Exception error so now we have to keep away from it
if (!/^(?:false|[dS]+)$/.check(encoded)) {
decoded = window.atob(encoded);
}

// `JSON.parse()` can throw a Syntax Error when
// it's given invalid JSON tokens to parse. So,
// it is very important wrap it in a strive/catch
strive {
parsed = window.JSON.parse<T>(decoded);

const parseResultType = typeof parsed
if (parseResultType !== "object") {
throw new TypeError(
`parsed decoded worth is a ${parseResultType} and never an object`
)
}
} catch (e) {
if (e instanceof Error) {
// rethrow the error (it is safer, higher)
// Explicitly let the calling code know one thing went unsuitable
if (e.identify === "SyntaxError") {
throw new Error(
"parse reset code failure: invalid JSON", { trigger: e }
);
} else if (e.identify === "TypeError") {
throw new Error(
"parse reset code dangerous outcome: non-object JSON", { trigger: e }
);
}
}
}
}

return parsed;
}

As you possibly can see above, the return kind annotation is now T | null as a substitute of simply T. If we take away the return kind annotation, the getResetCode() perform returns a sort of any. It seems that JSON.parse which returns any is messing up our return kind for this perform definition. You can see different manifestations of this downside on this video. Recently, Matt Pocock of TypeScript fame launched this npm package deal which solves this downside very properly. Other options exist as properly like this one. You ought to test them out!

JSON.parse can return null or false or true when handed a stringified type of these three values respectively with out throwing an error. Also, it could do the identical for any primitive JavaScript information kind worth, which can also be a legitimate JSON primitive worth (token) varieties (e.g., numbers, booleans, null).

JSON.parse(…) values for stringified primitive values

Which is why I added the road within the strive block as follows:

const parseResultType = typeof parsed
if (parseResultType !== "object") {
throw new TypeError(
`parsed decoded worth is a ${parseResultType} and never an object`
)
}

Finally, in distinction to the instance above, the right (alternate) behaviour is exhibited once you attempt to make use of the error variable in catch block of a strive/catch definition. The error variable will be something and never simply an Error object as a result of JavaScript lets you throw non-error “objects” comparable to strings or numbers and even null like so:

// Throwing a string as an error 🤣 - so humorous
strive {
throw "an error";
} catch (error) {
console.log(error); // "an error";
}

// Throwing a quantity as an error 😅 - even funnier
strive {
throw 1;
} catch (error) {
console.log(error); // 1;
}

So, this behaviour of JavaScript, though bizarre, is one thing that TypeScript has to accommodate, and in so doing, the default kind assigned to the error “object” is any. Just just like the return kind of JSON.parseis any, so is the default worth for the error “object” within the catch block. Kent C. Dodds explains this phenomenon fairly properly in his article.

Finally, typically the default primitive and object varieties equipped by TypeScript aren’t sufficient. You could outline a variable as a quantity kind, however particularly, you need non-negative integers or damaging float values. There isn’t any TypeScript kind that (by default) supplies this specificity and might validate the variable at runtime as assembly the factors.

3. TypeScript isn’t completely the identical throughout JavaScript environments

JavaScript thrives or exists in predominantly two environments: shopper (internet/desktop/cellular) and server. Also, wherever JavaScript can thrive, TypeScript may also thrive there too. But typically, the TypeScript kind definitions/declarations of the identical API widespread to the 2 environments, can differ, particularly now that NodeJS is making house for some browser-based APIs on the server.

For instance, the atob() base64 decoding perform that lives on the browser window can also be obtainable on NodeJS as of v16.0+. In any TypeScript file, the kind definitions for the atob() is loaded twice (as soon as for the NodeJS surroundings and as soon as for the browser surroundings). Therefore, utilizing it with out explicitly indicating which surroundings you’re utilizing it from will outcome within the unsuitable kind definition getting used. So, as a substitute of writing atob() , it’s a must to be specific by writing by referencing the Window object: window.atob() indicating you want the browser surroundings kind definition loaded and utilized by TypeScript and your code editor.

The identical factor occurs for setTimeout(). You must be specific concerning the surroundings you’re writing code for by referencing the Window object: window.setTimeout() (this is a matter for isomorphic JavaScript and server-side rendering — particularly again within the day) versus writing setTimeout(). Also, once you want the timer ID returned from setTimeout() , it’s difficult as a result of the return kind may fluctuate relying on the surroundings. So, it’s a must to do this like so:


// Isomorphic JavaScript (Server-side Rendered JavaScript)
let timerID: ReturnType<typeof setTimeout>;

timerID = setTimeout(() => console.log("prepared!"), 0);

// Client-side solely JavaScript
let timerID = window.setTimeout(() => console.log("prepared!"), 0);

These variations typically get in the best way of nice kind inference and typically security, particularly for software program engineers who construct reusable libraries and frameworks. What if the client-side JavaScript code utilizing window.setTimeout()is now speculated to be reused in a React Native surroundings? How will we take care of it?

It means now we have to write down the code defensively (in any other case referred to as kind narrowing) like so:

let timerID: ReturnType<typeof setTimeout>;
const timerDelay = 900;

// Detecting an Electron / NW.js surroundings on desktop
const isDesktopJSEnvironment = () => ;

// Detecting NativeScript / React Native on cellular
perform isMobileJSEnvironment () {
const $globals = typeof self === 'undefined' ? (world || {}) : (self || {})
const platform = $world.navigator
return (
typeof platform !== 'undefined' &&
platform.product.match(/^(ReactNative|NativeScript|NS)$/i) !== null
)
}

const callback = () => {
console.log("hi there there!");
};

// Defensively detect if now we have a `window` outlined utilizing an `if` assertion
// so we are able to use `window.setTimeout` safely with none kind errors

if (window !== undefined && window['document'] !== undefined) {

// Browser surroundings

// Let's say for less than the browser surroundings, i need cross the
// `callback` perform in string kind

// NOTE: we are able to solely do that on the Browser surroundings
const functionBodyRegex = /(?:perform|)(?:.*)(?:{([^}]*)})/;
const stringifiedCallback = callback.toString();
const extractedFunctionPhysique = stringifiedCallback.change(
functionBodyRegex,
"$1"
);

// Eval perform for some purpose:
timerID = window.setTimeout(extractedFunctionPhysique, timerDelay);
} else if (!isDesktopJSEnvironment()) {

// NodeJS or React Native or NativeScript surroundings

// But for these environments, i need cross the
// `callback` perform as is
timerID = setTimeout(callback, timerDelay);

if (isMobileJSEnvironment()) {
// Specifically React Native or NativeScript surroundings

// MORE CODE HERE
}
}

Here are real-world examples of utilizing defensive programming in well-liked open-source libraries (line numbers included for related code strains):

  1. axios — HTTP shopper library for NodeJS and Browser
  2. URISanity — Sanitizer for any URI used on the internet

4. TypeScript typically makes it tough to make use of sure ECMAScript (ES) expression syntaxes

As we write TypeScript code that will get the job carried out, there are locations in our codebase the place we use ES expression syntaxes that enhance and improve the developer expertise for gratis to readability. An instance of an ES expression syntax is destructuring. If you check out this venture constructed by the gifted, energetic, and improbable full-time open-source software program (OSS) developer: Sindre Sorhus.

The venture exists to increase the set of utility varieties (along with defaults like Pick, Extract, Omit, and Partial) obtainable to TypeScript to help software program builders in defining varieties that make it quite a bit simpler to work with TypeScript and ES6 syntax options and resolve errors a lot rapidly. In this venture, there’s a pull request (PR) that was submitted just lately (April 2023) that seeks to make it straightforward to take care of discriminated union varieties each time they’re destructured like so:

kind User = { id: string, username?: string };

interface SuccessResponse<D> {
standing: 'success';
information: D;
}

interface ErrorResponse<E> {
standing: 'error';
error: E;
}

// A server response can both include successful or an error worth
kind Response<P extends object, Q extends Error> =
SuccessResponse<P>
|
ErrorResponse<Q>;

// Assume the hard-coded worth for `customers` is dynamic and comes from a server.
const customers: Response<User[], Error> = {
standing: 'success',
information: [{ id: "123445670223345" }]
};

// Now destructure
const { standing, error, information } = customers;

// TS Error: Property `error` doesn't exist on `SuccessResponse<User[]>`

As quickly as you destructure (final line of code above), you get a TypeScript error: Property ‘error’ doesn’t exist on ‘SuccessResponse<User[]>.’

Now, the one cheap strategy to do away with that error from TypeScript is to not destructure in any respect after which use kind narrowing to proceed like so:

kind User = { id: string, username?: string };

interface SuccessResponse<D> {
standing: 'success';
information: D;
}

interface ErrorResponse<E> {
standing: 'error';
error: E;
}

// A server response can both include successful or an error worth
kind Response<P extends object, Q extends Error> =
SuccessResponse<P>
|
ErrorResponse<Q>;

// Assume the hard-coded worth for `customers` comes from a server HTTP response.
const customers: Response<User[], Error> = {
standing: 'error',
error: new Error("server crashed!")
};

// Don't destructure !!!
const response = customers;

// Narrow kind utilizing equality kind guard
if (response.standing === "error") {
console.error("Error: ", response.error);
}

This is one-way TypeScript takes away our capability to utilise ES6+ syntax choices like destructuring. Unless we may outline a customized utility kind that may wrap across the union kind and make it such that we are able to destructure safely with out having to take care of foolish TypeScript errors.

Such utility kind is the main target of this pull request and it is going to be an superior addition ought to or not it’s merged in.

Bury your TypeScript delusion!

Lots of TypeScript builders merely outline varieties, use them and assume they’re carried out.

I obtained the road above from this very insightful article I learn earlier than scripting this one. I saved nodding and agreeing with each level made in that article.

As a TypeScript developer, you continue to have work to do after you outline your varieties. You should take care of the very seemingly kind of considerations that TypeScript will not be exhibiting you upfront. You should write TypeScript defensively too.

Fault tolerance is an actual idea and applies to all types of software program, particularly software program that offers with information it receives exterior its static bounds. When making HTTP requests both from a shopper or from a server, you can’t be positive the server will at all times return information, so even with the kinds accurately outlined, you continue to must cater for faults the place the server crashed and can’t return legitimate information by coding defensively.

I’ve seen codebases the place the TypeScript interface and object varieties are outlined with non-obligatory fields, and non-obligatory chaining is abused to stupor. Function arguments should not validated for the likelihood that they received’t match the kind they’re annotated with. Here’s one instance:

kind ProductClasses = "electronics" | "garments" | "furnishings";

interface Product {
id: string;
worth: quantity;
identify: string;
vendor_ref: string;
class: ProductClasses
}

perform filterProductsByCategory (
merchandise: Product[],
class: ProductClasses
) {
// Are you and I positive that `merchandise` will probably be an array or perhaps null
// Since it comes from the a server HTTP response ??

// If merchandise is something aside from an array of "Product" objects,
// A kind error will instantly comply with.

return merchandise.filter((product) => {
return product.class === class;
});
}

// Assume the worth of `merchandise` got here from a server response with an error
const merchandise = undefined;

// Error: Cannot name perform `filter` of undefined
const derivedProducts = filterProductsByCategory(
merchandise,
"garments"
)

What I see most TypeScript builders do to repair the above is to begin abusing non-obligatory chaining like so:

kind ProductClasses = "electronics" | "garments" | "furnishings";

interface Product {
id: string;
worth: quantity;
identify: string;
vendor_ref: string;
class: ProductClasses
}

perform filterProductsByCategory (
merchandise?: Product[] | null,
class: ProductClasses
) {
// Are you and I positive that `merchandise` will probably be an array or perhaps null
// Since it comes from the a server HTTP response ??

// If merchandise is something aside from an array of "Product" objects,
// Well, i've to optionally chain the f*ck outta this shit!
return merchandise?.filter((product) => {
return product?.class === class;
});
}

// Assume the worth of `merchandise` got here from a server response with an error
const merchandise = null;

// 🤦🏾‍♂️ The solely factor you probably did was push the error additional up the decision stack
const derivedProducts = filterProductsByCategory(
merchandise,
"garments"
);

// Error: Cannot name property `0` of null
const firstProduct = derivedProducts[0];

If you need to study extra about the place and when non-obligatory chaining can be utilized accurately, see this text. There is a manner, nonetheless, to unravel this defensively:

Always setup a default worth for any variable whose worth is coming from exterior your codebases’ whole scope space (e.g., from a URL question parameter or URL hash or a server response payload or server response header)

kind ProductClasses = "electronics" | "garments" | "furnishings";

interface Product {
id: string;
worth: quantity;
identify: string;
vendor_ref: string;
class: ProductClasses
}

perform filterProductsByCategory (
merchandise: Product[],
class: ProductClasses
) {
// Are you and I positive that `merchandise` will probably be an array or perhaps null
// Since it comes from the a server HTTP response ??

// If merchandise is something aside from an array of "Product" objects,
// Weeeell, i obtained to optionally chain the f*ck outta this shit!
return merchandise.filter((product) => {
return product.class === class;
});
}

// Assume the worth of `merchandise` got here from a server response

// Use null coalescing right here as a substitute to set a default worth
const merchandise = null ?? [];

// 🤦🏾‍♂️ The solely factor you probably did was push the error additional up the decision stack
const derivedProducts = filterProductsByCategory(
merchandise,
"garments"
);

// No Errors !!!
const [ firstProduct ] = derivedProducts;

This will prevent a ton of stress, and also you don’t must needlessly abuse non-obligatory chaining. Another strategy to clear up this (overkill — if you’re utilizing it simply in a single place) will probably be to utilize the Maybe (Option<O>) monad (Did you understand that you need to use monads with TypeScript and {that a} Promise is a Future monad? Story for one more day!). Monads are worth objects that wrap variables and will be very useful in defensive programming and error dealing with. It can be utilized each in crucial language codebases in addition to purposeful language codebases. The Maybe monad additionally takes away the necessity for if statements which have truthiness checks.

The level is that you just don’t want non-obligatory chaining right here in any respect.

When utilizing or modifying any reference kind variable inside a extra native or equally native scope than the scope the reference kind variable was created in (particularly if the worth for the variable is from exterior your codebase whole scope space). Validate the kind first earlier than any direct use or mutation.

Sometimes, when writing code, you get a variable outlined exterior a perform definition that mutates it. TypeScript permits this to occur so long as the annotated or inference varieties align. However, what if the worth that’s used within the mutation isn’t hardcoded within the supply file however comes from a server response like so:

let scopedVariable: quantity[] = [1,2,3];

perform changeScopedVariable (newValue: quantity[]) {
scopedVariable = newValue;
}

// Assume `newValue` is outlined from a server response dynamically
const newValue = ["1", "2", "3", "4"];

// No errors right here since `newValue` is assumed to come back from a server response
changeScopedVariable(newValue);

You discover 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 worth of newValue is dynamic (from a server response) and never static (hardcoded), scopedVariable is mutated in place upon the chnageScopedVariable() name. This makes scopedVariable an array of strings and never an array of numbers.

To make sure that kind errors don’t happen wherever elsewhere, scopedVariable is used or interacted with, it’s a protected defensive programming mechanism to validate the parameter kind utilizing assertion signature kind guards earlier than direct mutation.

let scopedVariable: quantity[] = [1,2,3];

perform asNumberArray(record?: unknown[] | null): asserts record is quantity[] {

if (record === null || record === undefined) {
throw new Error("=: not an array");
}

const whole = record.scale back<quantity>((sum, recordItem) => {
return sum + (recordItem as quantity)
}, 0);

if (Number.isNaN(whole) || typeof whole !== "quantity") {
throw new Error("=: not an array of numbers");
}
}

perform changeScopedVariable (newValue: unknown[]) {
// Validate `newValue` as an array of numbers
asNumberArray(newValue);

scopedVariable = newValue;
}

// Assume `newValue` is outlined from a server response dynamically
const newValue = ["1", "2", "3", "4"];

// No errors right here since `newValue` is assumed to come back from a server response
changeScopedVariable(newValue);

One factor you’d have seen (from the code snippet above) is that I’ve modified the kind annotation for the parameter of the changeScopedVariable() perform from quantity[] to unknown[] to replicate the truth that newValue could or will not be an array of numbers at runtime and work issues out from that standpoint.

You may also use this validation to find out if parameters (whether or not non-obligatory or necessary) for features are of the right kind at runtime.

Don’t abuse using strive/catch blocks! Verify that an error is definitely thrown by a line or strains of code earlier than wrapping them in a strive/catch block. The solely error dealing with software program wants is for errors that both have a excessive probability of occurring or will truly happen at runtime.

There are instances once you need to deal with an exception or error. However, care have to be taken to not overuse strive/catch blocks, although they’re nice instruments for defensive programming. Read up on this text to seek out out extra.

One function I might like to see in future variations of TypeScript that can additional help the kind security targets of TypeScript is the throws assertion in Java. It is without doubt one of the greatest elements of Java and has a spot in trendy TypeScript. It will restrict the abuse of strive/catch blocks and additional streamline the method of error dealing with in TypeScript and make sure that it’s constant all through the codebase. It will even considerably scale back the cases of “Unhandled Exception” at runtime. It seems this was proposed in 2016 on the official GitHub TypeScript repo difficulty board however was explicitly denied. However, I urge the TypeScript crew to revisit it quickly.

In this text of mine, I wrote down a number of recommendations on organising higher error dealing with and debugging and the way vital it’s to deal with errors nearer to the entry level of any software program program. One tip I unnoticed mistakenly is making certain constant return varieties from all technique and performance calls.

This is an instance of the unsuitable methods individuals deal with error eventualities in JavaScript by not making certain constant return varieties like so:

interface User {
id: string;
avatar_url: string;
e-mail: string;
full_name: string;
}

interface Task "medium"

perform getTasksFor(person: User): Promise<Task[]> {
const duties: Task[] = [
{ assignee: user, name: "Do Something", priority: "low" }
];
return Promise.resolve(duties);
}

async perform getUserTasks (person: User): boolean | undefined | Task[] {
// Defensively make sure that `person` is outlined
if (!person) {
// Return early as a result of we will not proceed if `person` is null or undefined
return false; // BAD MOVE!
}

let duties: Task[];

strive {
duties = await getTasksFor(person);
} catch (_) {
return; // ALSO, BAD MOVE!
}

return duties;
}

There are just a few issues with above code snippet. The first downside is that the duties variable doesn’t have a default worth. The second downside is error circumstances should not raised explicitly. The third downside is that the getUserTasks perform can return three totally different information varieties: boolean, undefined, and an array of duties. It ought to return just one information kind or at most two as a discriminated union like so (however we’re nonetheless not out of the woods but):


interface User {
id: string;
avatar_url: string;
e-mail: string;
full_name: string;
}

interface Task "medium"

perform getTasksFor(person: User): Promise<Task[]> {
const duties: Task[] = [
{ assignee: user, name: "Undo Something else", priority: "high" }
];
return Promise.resolve(duties);
}

async perform getUserTasks (person: User): Promise<Task[]> {
// Defensively setup a default worth
let duties: Task[] = [];

// Defensively make sure that `person` is outlined
if (!person) {
// Return early as a result of we will not proceed if `person` is null or undefined
return duties; // STILL A BAD MOVE!!
}

strive {
duties = await getTasksFor(person);
} catch (_) {
duties = []; // ALSO, STILL A BAD MOVE!!
}

return duties;
}

The modifications made to the code snippet (above) are vital however we haven’t mounted the second downside we recognized. Depending on how we select to proceed, the second downside will not be an issue as a result of we are able to at all times use an invariant to test whether or not or not the duties array returned by getUserTasks() is empty and throw an error whether it is. But what number of software program engineers in the actual world make use of invariants ? Well, not many from the place I’m sitting. Also, not fixing this downside violates the nice rule of thumb: crash early, crash typically. Finally, it additionally improves code readability, comprehension and reduces cognitive load.

So, now we have to lift errors explicitly like so:

interface User {
id: string;
avatar_url: string;
e-mail: string;
full_name: string;
}

interface Task "medium"

perform getTasksFor(person: User): Promise<Task[]> {
const duties: Task[] = [
{ assignee: user, name: "Undo Something else", priority: "high" }
];
return Promise.resolve(duties);
}

async perform getUserTasks (person: User): Promise<Task[]>, throws Error {
// Defensively setup a default worth
let duties: Task[] = [];

// Defensively make sure that person is outlined
if (!person) {
// Return early as a result of we will not proceed if `person` is null or undefined
throw new Error("person isn't outlined"); // GOOD AND GREAT MOVE!
}

strive {
duties = await getTasksFor(person);
} catch (error) {
if (error instanceof Error) {
// ALSO, GOOD AND GREAT MOVE
throw new Error("duties not retrieved for person", { trigger: error });
}
}

return duties;
}

A accountable use of TypeScript requires that extra ensures are supplied for and which will probably be utilised at runtime. The naked minimal of simply defining and utilizing varieties and subtypes isn’t sufficient.

Furthermore, TypeScript configuration must be taken severely when organising TypeScript.

Ensure you utilize TypeScript’s strictNullChecks compiler flag choice to ensure your code isn’t doing any unlawful kind conversions or implicit kind coercions. This will catch a whole lot of errors that may in any other case be laborious to trace down.

Also, make sure that the noPropertyAccessFromIndexSignature and noUncheckedIndexedAccess compiler flag choices are additionally set for significantly better kind security with object literals and arrays in TypeScript.

Finally, I’m not in any manner excluding using design by contract in constructing software program programs that speak to at least one one other. Defensive programming helps in conditions the place actuality strikes a blow (e.g., service timeouts) and the contract is quickly and/or extensively damaged.

Enjoy coding defensively!

You may also like

Leave a Comment