Reputation: 1684
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
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;
Upvotes: 47
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:
myMap.get('test')!
(be very careful doing this as code in between the .set
and .get
could change/delete that item)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