microsoft/TypeScript

Object is possibly 'undefined' error when using typed map value in closure

borisovg opened this issue ยท 10 comments

TypeScript Version: 4.0.3

Search Terms: "Object is possibly 'undefined' map"

Code

type Thing = { data: any };

const things: Map<string, Thing> = new Map();

function add_thing (id: string, data: any) {
    let thing = things.get(id);

    if (typeof thing === 'undefined') {
        thing = { data };
        things.set(id, thing);
    }

    return () => thing.data;
}

Expected behavior:
No errors - it is not possible for "thing" to be undefined based on the above code.

Actual behavior:
Object is possibly 'undefined' error.

If things is instead defined as const things = new Map(); then there is no error.

If it is assigned to another constant like this const thing2 = thing; and that is used in the closure then there is also no error.

Playground Link: https://www.typescriptlang.org/play?#code/C4TwDgpgBAKgFgSwHYHMoF4oG8oBMCGw+AXFPkiFAL4DcAUHQMYD2SAzsFMIqm6QLL4wAHg4AnZCgA0sHigB8GKEggB3KILAAKAJT06AMwCuSRsASsyuXAH1ukqFoS5S4yTIJFS5EDux0oQKgAGwhOe1QlCJQ2ADoUMKdcPQYgqAQDR1BIZkzojHRMAHITXAgDZAhcIr8sALSg-MwcT3xqegbGuTi2ROcZaJS0qlSgsTCjMSRHP3RFaNjW+iogA

Related Issues: #7719

Duplicate of #9998.

Quick workaround: Store it in a new const variable:

const cthing = thing;
return () => cthing.data;

@MartinJohns thanks I am aware of this workaround - edited the description.

It's still a duplicate of #9998. :-)

If things is instead defined as const things = new Map(); then there is no error.

If you declare it like this, then the type is inferred to be Map<any, any>. As a result the type of thing is any, and you opted out of all type-safety checks. That's why you don't get an error in this case.

Another nasty workaround:

const thing = things.get(id) ?? (things.set(id, { data }), things.get(id)!);

return () => thing.data;

(Please don't use this.)

I guess my objection to 9998 is the "discussion" label there when this is clearly a bug IMHO. That and my example looks way more RWC to me than the code in that ticket, although I did simplify it a bit for brevity. ;-)

The least ugly workaround seems to be this: return () => thing!.data;

That's why we need Map.prototype.emplace. You might consider using a polyfill of it.

function add_thing (id: string, data: any) {
    return things.emplace(id, {insert: () => ({ data })});
}

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

This should work without the need of a new const
return () => thing!.data;

I would do like this:

interface IMap<K, V> extends Map<K, V> {
  get(key: K): V;
}

type Thing = { data: any };

const things: IMap<string, Thing> = new Map();