DEV Community

Cover image for You Probably Know This Bug, but why does it Always Slip Through?
Lukas Mauser Subscriber for Wimadev

Posted on • Edited on

You Probably Know This Bug, but why does it Always Slip Through?

TypeScript does not catch this in strict mode

Does this error look familiar?

Image description

Trying to access undefined objects is one of the most frequent errors in JavaScript.

Even if you use TypeScript you are not guaranteed to avoid this Bug.

And I'm not talking about sloppy TypeScipt usage here, like throwing in any, using @ts-ignore or type assertions with as string...

I'm talking about using TypeScript without exceptions in strict mode.

The way it's supposed to be used. And this bug will still slip through.

Why is that?

And what can you do to make your code more robust?

Why does TypeScript miss this?

Let's start with some simple examples:

// 1. Arrays

const array: { id:string }[] = []
//...
console.log(array[0].id) // this throws an error but won't be caught by TS


// 2. Objects

const object: Record<string, string> = {}
//...
console.log(object['randomKey'].length) // this throws an error but won't be caught by TS

Enter fullscreen mode Exit fullscreen mode

In both scenarios an error will be thrown but most TypeScript setups would not have seen it coming.

This is because TypeScript can not reliably track every index in all code paths between the initialization and property access of your array or object and therefore simply doesn't do it.

There are options to deal with this, but each of them has some downsides.

What can you do?

Testing

While you could potentially catch this bug in a test, you can not solely rely on it.

It is one of these cases, where 100% coverage doesn't tell you anything.

An array that is defined at index 0, might be undefined at index 1. You'd have to test all possible indices which is literally impossible.

So relying on tests means, that you either have already identified the critical step in your code beforehand or you accidentally stumble upon it in the context of a different test.

Maps

Use maps instead of objects or arrays. Maps can be used similar to objects or arrays, but type safe.

// instead of this:
const object: Record<string, string> = {}
console.log(object['randomKey'].length) // throws error

// do this:
const map = new Map<string, string>()
const str = map.get('randomKey') // returns string | undefined
console.log(str.length) // TS catches this, because str could be undefined

Enter fullscreen mode Exit fullscreen mode

However, one big downsides of using a map, is that it needs to be converted to an object before it can be serialized to JSON and vise versa.

This can slow down your application when dealing with big datasets and introduce an additional layer of complexity.

Another reason against maps is that arrays and objects are just simpler to use and less verbose.

Go Beyond Strict

There is a TypeScript compiler option called noUncheckedIndexedAccess introduced in version 4.2.

// tsconfig.json
{
  // ...
  "compilerOptions": {
    // ...
    "noUncheckedIndexedAccess":true
  }
}
Enter fullscreen mode Exit fullscreen mode

This option will warn you, whenever you access a property via an index.

const array: { id:string }[] = []
//...
console.log(array[0].id) // TS will warn you: 'array[0]' is possibly 'undefined'.

Enter fullscreen mode Exit fullscreen mode

Solved?

Partially.

Like mentioned before, the TypeScript compiler has it's limits... And adding noUncheckedIndexedAccess leads to a lot of false positives:

const array : {id: string}[] = [ { id: 'id1' }]
console.log(array[0].id) // this throws a TS error, even though array[0] is defined

// this won't work either
if(array.length > 0) console.log(array[0].id) // throw TS error

// also C-style loops won't work anymore
for (let i = 0; i < array.length; i++) {
    console.log(array[i].id); // throws TS error
}

Enter fullscreen mode Exit fullscreen mode

It's personal preference if you want to double check every indexed access, even though sometimes you are already sure that it is defined. That's why this option is disabled in strict mode.

The Solution? A Combination

The "can not read property of undefined" bug is a real thing that is missed all the time. The problem is, that this bug is hard to detect and even tests won't save you here.

To make your code more robust, you can use a combination of the tools listed above.

Use tests anyways and maybe catch something in the net that you did not expect.

Consider maps where they make sense in your codebase.

And use TypeScripts noUncheckedIndexedAccess compiler option as an additional safety net.

If you don't like the unnecessary verbose syntax that comes with it, you can still toggle this option every now and then during development, just to identify potential vulnerabilities in your code.

Side note

Need professional support on your Vue.js or Nuxt.js project? Reach out to me via https://nuxt.wimadev.de

Top comments (1)

Collapse
 
code42cate profile image
Jonas Scholz

I honestly hate not having all of the strict compilerOptions on. I usually have at least those in every config:

{
    "strict": true,
    "allowUnusedLabels": false,
    "allowUnreachableCode": false,
    "exactOptionalPropertyTypes": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
}
Enter fullscreen mode Exit fullscreen mode