W4G1
W4G1

Reputation: 1684

Object is possibly 'undefined' using ES6 Map.get() right after Map.set()

Running the following snipped I'm getting a Object is possibly 'undefined'. ts(2532) error.

const myMap = new Map<string, number>();

myMap.set('test', 1);

// Object is possibly 'undefined'.
myMap.get('test') / 2;

I'm using TypeScript 4.5.4 (also checked on 4.4.4)

If this is intended, could someone explain how the example above could possibly still be undefined?

Upvotes: 27

Views: 28666

Answers (2)

jcalz
jcalz

Reputation: 329198

This is somewhat of a design limitation of TypeScript; the compiler can't keep track of the state of the Map that way. There's a (fairly old) feature request at microsoft/TypeScript#9619 to support control flow analysis for Map methods.

It can't be done without changing the language, though. Currently Map<K, V> is declared as an interface. The set() method has a return type of this (because calling set() returns the same Map instance). And the get() method has a return type of V | undefined.

But there's not really a way to say that set() mutates the state of the instance so that get() with the "right" keys will return just V instead of V | undefined. In some sense you want calling myMap.set("test", 1) to change the type of myMap from Map<string, number> (which doesn't know what keys have actual values) to something like Map<string, number> & {get("test"): number}. Presumably you'd want myMap.delete("test") to change the type to Map<string, number> & {get("test"): undefined}. But TypeScript doesn't let you represent arbitrary type mutations like this. There are assertion methods but there are lots of caveats, the biggest of which is that they only strictly narrow a type, and if the compiler doesn't see the behavior as narrowing it won't work.

So right now it's basically a limitation. That doesn't mean that myMap.get('test') can actually be undefined, just that the compiler doesn't know this.


Rather than wait for some possible future version of TypeScript to support this use case, you might want to work around it or refactor. The easiest workaround is to just accept that you're cleverer than the compiler and use a non-null assertion to tell it that a value cannot be null or undefined:

const myMap = new Map<string, number>();
myMap.set("test", 1);
myMap.get("test")! / 2; // <-- no error

If you really need the compiler to keep track of such things, then you can refactor to use method chaining; instead of having a single object named myMap whose state is different every time you call set(), you use multiple objects, each of which has a single constant state... when you call set() you get a new object. It could look like this:

const init = new MyMap();
//const init: MyMap<{}> 

const afterSet = init.set("test", 1); 
//const afterSet: MyMap<Record<"test", number>>

const val = afterSet.get("test") / 2;

Or like this:

const m = new MyMap().set("test", 1).set("foo", "abc").set("baz", false);
m.get("foo").toUpperCase();
m.get("test").toFixed();
m.get("blah") // error! not a valid key

Here's one possible implementation (only dealing with get() and set() but you could implement other methods too):

interface MyMap<T extends object = {}> {
  set<K extends keyof T>(k: K, v: T[K]): this;
  set<K extends string, V>(k: Exclude<K, keyof T>, v: V): MyMap<T & Record<K, V>>;
  get<K extends string>(k: K): K extends keyof T ? T[K] : undefined;
}
const MyMap = Map as new () => MyMap;

Playground link to code

Upvotes: 47

ejose19
ejose19

Reputation: 599

Typescript doesn't keep track of the .set .get references, and the default signature of .get is T | undefined, so you can either:

  • assert myMap.get('test')! (be very careful doing this as code in between the .set and .get could change/delete that item)
  • handle the possibility of the item being undefined and act accordingly
const myMap = new Map<string, number>();

myMap.set('test', 1);

// Object is possibly 'undefined'.
const item = myMap.get('test');
if (item !== undefined) {
  item / 2;
} else {
  throw new Error('Item is undefined');
}

Upvotes: 9

Related Questions