5

I'm trying to wrap my head around flow and I struggle to make it work with ES6's Map

Consider this simple case (live demo):

// create a new map
const m = new Map();

m.set('value', 5);

console.log(m.get('value') * 5)

flow throws:

console.log(m.get('value') * 5)
               ^ Cannot perform arithmetic operation because undefined [1] is not a number.
References:
[LIB] static/v0.72.0/flowlib/core.js:532:     get(key: K): V | void;
                                                               ^ [1]

I also tried:

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

m.set('value', 5);

console.log(m.get('value') * 5)

But I got the same error

I believe this is because flow thinks that the value can also be something else than a number, so I tried to wrap the map with a strict setter and getter (live demo):

type MyMapType = {
    set: (key: string, value: number) => MyMapType,
    get: (key: string) => number
};

function MyMap() : MyMapType {
    const map = new Map();

    return {
        set (key: string, value: number) {
          map.set(key, value);
          return this;
        },
        get (key: string) {
          return map.get(key);
        }
    }
}


const m = MyMap();

m.set('value', 5);

const n = m.get('value');

console.log(n * 2);

but then I got:

get (key: string) {
^ Cannot return object literal because undefined [1] is incompatible 
with number [2] in the return value of property `get`.
References:
[LIB] static/v0.72.0/flowlib/core.js:532:     get(key: K): V | void;
                                                               ^ [1]
get: (key: string) => number                            ^ [2]

How can I tell flow that I only deal with a Map of numbers?

Edit:

Typescript approach makes more senses to me, it throws on set instead on get.

// TypeScript

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

m.set('value', 'no-number'); // << throws on set, not on get

console.log(m.get('value') * 2);

Is there a way to make Flow behave the same way?

Asaf Katz
  • 4,608
  • 4
  • 37
  • 42
  • 1
    Actually if you turn on _strictNullChecks_ in typescript you'll get the same behavior on `get` because return value is possibly undefined. – Aleksey L. May 16 '18 at 05:11

2 Answers2

8

What Flow is trying to tell you is that by calling map.get(key), .get(...) may (V) or may not (void) return something out of that map. If the key is not found in the map, then the call to .get(...) will return undefined. To get around this, you need to handle the case where something is returned undefined. Here's a few ways to do it:

(Try)

const m = new Map();

m.set('value', 5);

// Throw if a value is not found
const getOrThrow = (map, key) => {
  const val = map.get(key)
  if (val == null) {
    throw new Error("Uh-oh, key not found") 
  }
  return val
}

// Return a default value if the key is not found
const getOrDefault = (map, key, defaultValue) => {
  const val = map.get(key)
  return val == null ? defaultValue : val
}

console.log(getOrThrow(m, 'value') * 5)
console.log(getOrDefault(m, 'value', 1) * 5)

The reason that map.get(key) is typed as V | void is the map might not contain a value at that key. If it doesn't have a value at the key, then you'll throw a runtime error. The Flow developers decided they would rather force the developer (you and me) to think about the problem while we're writing the code then find out at runtime.

James Kraus
  • 3,349
  • 20
  • 27
  • Thank you for the quick answer, James. the getter approach does seem work. What I was hoping for is a way to specify a special type on Map that will prevent the ability to *set* non-number value, see the typescript equivalate I've added to my question – Asaf Katz May 15 '18 at 17:31
  • 1
    Nope, sorry. Flow's typing seems correct in this case. It doesn't have a good way to statically know that the map will return a value for any given key. For trivial examples such as the above, it seems easy, but it really isn't. For example, typescript fails to throw an error for this: `m.get('non-existent-key') * 2`. Flow has chosen to type that result as maybe null so you have a chance of catching the error. – James Kraus May 15 '18 at 18:02
  • Interesting... Thanks James (: – Asaf Katz May 15 '18 at 19:28
  • 1
    @JamesKraus Actually if you turn on _strictNullChecks_ in typescript you'll get the same behavior on `get` – Aleksey L. May 16 '18 at 05:13
  • @AlekseyL can you also turn it off for Flow? – Asaf Katz May 16 '18 at 10:43
  • 1
    I'm not aware of such an option and I wouldn't do it globally (in typescript you can use non null assertion operator `!` to tell the compiler that you're sure that the value **is** defined) – Aleksey L. May 16 '18 at 12:44
1

Random and pretty late, but was searching and came up with this for my own use cases when I didn't see it mentioned:

const specialIdMap = new Map<SpecialId, Set<SpecialId>>();
const set : Set<SpecialId> = specialIdMap.get(uniqueSpecialId) || new Set();

and this saves quite a lot of boilerplate of checking if null and/or whatever. Of course, this only works if you also do not rely on a falsy value. Alternatively, you could use the new ?? operator.

rob2d
  • 964
  • 9
  • 16