TypeScript does not catch this in strict mode
Does this error look familiar?
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
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
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
}
}
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'.
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
}
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)
I honestly hate not having all of the strict compilerOptions on. I usually have at least those in every config: