Initial Problem
Accessing object properties, especially dynamically, is a common cause of errors in programs. That’s because there is no guarantee that those keys are present on the object. In JavaScript, that means you could be getting unexpectedly undefined
values back.
const obj : Record<string, string> = buildSomeObject
const myKey = "myKey"
// Potential runtime error! There is no guarantee the
// key is present in the object
obj[myKey].length
TypeScript is designed to help you catch these sorts of errors in development instead of in production (make sure you have the noUncheckedAccess config turned on). The type-checker sees obj[myKey]
and gives it a type of string | undefined
, correctly detecting that there is an edge-case here.
Type narrowing
Calling methods on obj[myKey]
could crash our program. TypeScript, being a good assistant, won’t let us do that. It wants us to handle the edge-case.
One of the cool features of TypeScript is that it can automatically figure out that we’ve checked for edge-cases and narrow the type for us. Now we have a zone in our code where a value is no longer string | undefined
, but rather is just string.
const someUnsafeVal = ... // string | undefined
if (someUnsafeVal !== undefined) {
// here someUnsafeVal is guaranteed to be string
}
Typescript isn’t happy
So what if we try the type narrowing trick with our dynamic object access code? Somehow that doesn’t seem to work. TypeScript still thinks our value is undefined inside the condition body, even though we explicitly checked that it wasn’t.
if (obj[myKey] !== undefined) {
obj[myKey] // still has type string | undefined
}
Introducing an intermediate variable
The problem is that the narrowed type isn’t “sticking”, so the next time TypeScript sees that dynamic key lookup it re-calculates its type and thinks it’s potentially undefined.
The solution is to introduce an intermediate variable that the narrowed type can stick to.
const nestedVal = obj[myKey] // has type string | undefined
if (nestedVal !== undefined) {
nestedVal // has type string
}
Multiple nesting
This trick works for multiple nesting too:
type Child = Record <string, string>
type Parent = Record<string, Child>
const parent : Parent = buildSomeObject
const parentKey = "parentKey"
const childKey = "childKey"
const child = parent[parentKey] // Child | undefined
if (child !== undefined) {
// child is guaranteed to be a Child
const childProp = child[childKey] // string | undefined
if (child[childKey] !== undefined) {
// childProp is a guaranteed string
}
}
Helping out the type-checker
TypeScript is great at pointing out edge-cases in our code that we forgot to handle. Like any assistant, we need to make sure we communicate properly to it to get the best results. One of the ways we can do this is by introducing an intermediate variable for our dynamic object accesses.
As a team, we can write code with fewer runtime errors!