Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Object is possibly 'undefined'." in TypeScript

Tags:

typescript

The code below throws Object is possibly 'undefined'. at obj.field1 += ',' + v;. TypeScript says the obj may be undefined, but the obj can not be undefined at this point because {field1: 'testtest'} is assigned in case map.get(key) returns undefined.

Why I got this error? How can I fix this?

interface TestIF {
  field1: string;
}

export class MyClass {
  test(): void {
    const map1: Map<string, TestIF> = new Map();
    const map2: Map<string, string> = new Map();

    const key = 'mapkey';
    let obj = map1.get(key);
    if (obj == null) {
      obj = {field1: 'testtest'};
      map1.set(key, obj);
    }

    map2.forEach( v => {
      obj.field1 += ',' + v;  // Object is possibly 'undefined'.
    });
  }
}
like image 290
N.F. Avatar asked Aug 07 '19 00:08

N.F.


People also ask

How do you handle undefined in TypeScript?

To make a variable null we must assign null value to it as by default in typescript unassigned values are termed undefined. We can use typeof or '==' or '===' to check if a variable is null or undefined in typescript.

How do you prevent undefined in TypeScript?

To avoid undefined values when using or accessing the optional object properties, the basic idea is to check the property value using an if conditional statement or the optional chaining operator before accessing the object property in TypeScript.

Can strings be undefined TypeScript?

The typescript compiler performs strict null checks, which means you can't pass a string | undefined variable into a method that expects a string .

Is null in TypeScript?

TypeScript has two special types, Null and Undefined, that have the values null and undefined respectively. Previously it was not possible to explicitly name these types, but null and undefined may now be used as type names regardless of type checking mode.


2 Answers

The error happens because control flow analysis is difficult, especially when the information you want to keep track of cannot be represented in the type system.

In the general case, the compiler really cannot figure out much about what happens when functions accept callbacks that mutate variables. The control flow inside such functions is a complete mystery; maybe the callback will be called immediately and exactly once. Maybe the callback will never be called. Maybe the callback will be called a million times. Or maybe it will be called asynchronously, far in the future. Since the general case is so hopeless, the compiler doesn't even really try. It uses some heuristics which work for a lot of cases, and which also necessarily fail for a lot of cases.
You picked one of the failures.

The heuristic used here is that inside of a callback, all narrowings which occurred in the wider scope are reset. That does reasonable things for code like this:

// who knows when this actually calls its callback?
declare function mysteryCallbackCaller(cb: () => void): void;

let a: string | undefined = "hey";
mysteryCallbackCaller(() => a.charAt(0)); // error!  a may be undefined
a = undefined;

The compiler doesn't know when or if () => a.charAt(0) gets invoked. If it gets invoked immediately when mysteryCallbackCaller() is called, then a will be defined. But if it gets called sometime later, a may be undefined. Since the compiler cannot guarantee safety here, it reports an error.


So what can we do to address this issue in your case? There are two main solutions I can think of. One is to just tell the compiler that it's wrong and that you are sure that obj will be defined. This can be done using the ! non-null assertion operator:

map2.forEach(v => {
  obj!.field1 += "," + v; // okay now
});

This works with no compile time error. The caveat to this solution is that the responsibility for ensuring obj is defined is now only yours and not the compiler's. If you change the preceding code and obj truly is possibly undefined, then the type assertion will still suppress the error, and you'll have issues at runtime.


The other solution is to change what you're doing so that the compiler can verify that your callback is safe. The easiest way to do that is to use a new variable:

// over here the compiler knows obj is defined
const constObj = obj; // type is inferred as TestIF
map2.forEach(v => {
  constObj.field1 += "," + v; // okay, constObj is TestIF, so this works
});

All I've done here is assign obj to constObj. But at the time this assignment takes place, obj cannot be undefined. Thus constObj is just a TestIF, and not a TestIF | undefined. And since constObj is never reassigned and cannot be undefined, the rest of the code works.


Okay, hope that helps. Good luck!

Link to code

like image 58
jcalz Avatar answered Sep 17 '22 20:09

jcalz


The Map's get method is defined as Map<string, TestIF>.get(key: string): TestIF | undefined, so when you set obj, it's type is TestIF | undefined.

When you (re-set) obj's type inside the if block, it's in a different scope. When you read obj inside the forEach, it's also in another scope. The TypeScript compiler is unable to establish the correct type in the changed scopes.

Consider this (working) code:

    const key = 'mapkey';
    let obj: TestIF; // Create variable with a Type
    if (map1.has(key)) { // We know (with certainty) that obj exists!
      obj = map1.get(key) as TestIF; // We use 'as' because we know it can't be Undefined
    } else {
      obj = { field1: 'testtest' };
      map1.set(key, obj);
    }

Even though Map.get() will always return V | undefined, when we used as, we forced TypeScript to treat is as V. I use as with caution, but in this case we know it exists as we have called Map.has() to check it's existence.

Also, I want to stress that (obj === undefined) is much better than (obj == null), which just checks for falsyness. [more info]

like image 21
JD Byrnes Avatar answered Sep 20 '22 20:09

JD Byrnes