TypeScript: Flow analysis doesn't work with es6 collections 'has' method
TypeScript Version: 2.1.1
Code
const x = new Map<string, string>();
x.set("key", "value");
if (x.has("key")) {
const y : string = x.get("key"); // error: y is string | undefined, not string
}
Expected behavior: y is narrowed down to string Actual behavior: y is still string | undefined even after checking if the map has that key
About this issue
- Original URL
- State: open
- Created 8 years ago
- Reactions: 167
- Comments: 35 (4 by maintainers)
Links to this issue
Commits related to this issue
- ugh https://github.com/microsoft/TypeScript/issues/13086 — committed to icecream17/solver by icecream17 3 years ago
- fix: fix TypeScript error TypeScript doesn't detect Map.has: https://github.com/microsoft/TypeScript/issues/13086 — committed to crazyo/cdktf-aws-cdk by ansgarm 2 years ago
@DanielRosenwasser I think you’re a bit overcomplicating the stuff.
Anyway, these all are workarounds. What’s need to be fixed if flow analysis.
Actually, you can get this to work better with another overload.
This much, much, much, much more tricky than it looks because you also have to define how long the
hascheck should last.@chapterjason you could do
it’s frustrating to see the mediocrity of some api in js the Set and Map should have implemented natively
.get().find()Map.prototype.getshould return strictlyVor throw error andMap.prototype.findshould returnV|undefinedIdk who approved this es2015 API feature, but they coded lazily.Will the awesome new control flow analysis in TypeScript beta 4.4 make this possible/easier?
Ok well I found my answer which is that TypeScript does not handle this correctly with objects:
Just stumbled upon this issue, wanted to note that this is quite hard to get right. For example the workarounds do not take into account that the entry might be
deleted from the map between thehasand theget.What? What does truthiness have to do with any of this? As for your snippet:
I beg to differ:
I don’t see how a map should be different from an object. Internally, they’re both hash maps, they just have different APIs for accessing their members (and Map also maintains an ordered index of entries, but that’s irrelevant). In both cases someone can technically use type casting or simply vanilla Javascript to inject values that Typescript does not expect. Nevertheless, that doesn’t stop Typescript in my above example from correctly assuming that the internal state of object/hashmap
catwill have an entry with the keybreeddefined.Wouldn’t you have the exact same problem with an object index lookup when the members on the object are optional? Does TypeScript handle that case correctly?
Yet it will infer the type of variables declared with
let(for all branches) without explicit casts just fine. So it absolutely tracks the state of values in order to function. Do note I am talking aboutMaps with generics set (derived fromas constobjects and arrays for example), not the freeform dictionary use, so there is no ambiguity for values. The callmapExample.delete("foo")should be a compile-time error, because typescript knows the result ofMap.delete(key)makes the value effectivelyundefined, which is not of typenumber. But so far not evenReadonlyMapcan derive its type from anas constobject. And as dictionaries they are already pretty annoying to use due to not being serializable, typescript making to write extra boilerplate code for convenience methods doesn’t help it either. Instead it nudges to write in the old way of using objects as dictionaries, which has all problems of key access asMapplus extra more but without compile-time errors.I wasn’t talking only about arrays, objects are collections too. You have to validate the value of every single dot/index notation access (do note the key might be a getter, so you have to call with
Object.getOwnPropertyDescriptor()instead). And in case of global objects, which are frequent targets for implicit changes, you have to write a wasm module to validate those and run it before each call (without implicit changes ofc, as the symbol will be invalid on the second validator call).if I may add, by the very same logic you might as well write type guards for every variable no matter the type, because you never know if someone decided to shoehorn an
undefinedinto your string variable.Typescript is 100% compile-time. If you don’t trust compile-time type checks, why use Typescript at all?
That’s a pretty terrible example because
Maphas generics for key and value types. So something like this won’t even pass the compilation:But typescript is still unsure of the value:
In both branches it is
number | undefined.@MastroLindus You can workaround this by using:
By the same logic you have to use type guards for any collection property access because you never know if one returns
undefinedat runtime. And also validate that the runtime typeguard function wasn’t tampered with before each call.@jeysal that’s true, but I believe 90% of use cases are just
hascheck andgetright after the check.