Flow: call of methods `get` undefined

Created on 4 Nov 2016  路  14Comments  路  Source: facebook/flow

I have a map of type:

Map

I call the get() methods after I run a has() check on the Map Object, and flow warns me about incompatible type. Why?

Here is part of my code:

if (sectionBody.has(key)) {
    const prevVal: string | string[] = sectionBody.get(key);
    sectionBody.set(key, [].concat(prevVal, value));
}

And this is the error message

74:                    const prevVal: string | string[] = sectionBody.get(key);
                                                           ^^^^^^^^^^^^^^^^^^^^ call of method `get`
 74:                    const prevVal: string | string[] = sectionBody.get(key);
                                                           ^^^^^^^^^^^^^^^^^^^^ undefined. This type is incompatible with
 74:                    const prevVal: string | string[] = sectionBody.get(key);
                                       ^^^^^^^^^^^^^^^^^ union: string | array type
  Member 1:
   74:                  const prevVal: string | string[] = sectionBody.get(key);
                                       ^^^^^^ string
  Error:
   74:                  const prevVal: string | string[] = sectionBody.get(key);
                                                           ^^^^^^^^^^^^^^^^^^^^ undefined. This type is incompatible with
   74:                  const prevVal: string | string[] = sectionBody.get(key);
                                       ^^^^^^ string
  Member 2:
   74:                  const prevVal: string | string[] = sectionBody.get(key);
                                                ^^^^^^^^ array type
  Error:
   74:                  const prevVal: string | string[] = sectionBody.get(key);
                                                           ^^^^^^^^^^^^^^^^^^^^ undefined. This type is incompatible with
   74:                  const prevVal: string | string[] = sectionBody.get(key);
                                                ^^^^^^^^ array type
feature request

Most helpful comment

This problem makes Flow incompatible with the technique of using WeakMaps to manage truly private data associated with ES6 class instances.

(The technique is discussed by Dr. Rauschmayer here and in more detail by Nicholas C. Zakas here.)

Currently I can't find any decent workaround.

All 14 comments

getreturns ?T no matter what, so calling has before has no effect

is there a work around for this?

@gre you are right, there is no reason for WeakMap to be any different

gre, thanks for the work around. However, I really want Flow to "understands common JavaScript idioms and very dynamic code".

@steambap I don't think flow will easily know that has and get are linked each other. the fact that if has(key) is true, get(key) should be the result.
I can't even think of any type language that have this possible.

Usually, in some other functional language, you have get() that returns an Option[T] , that you can safely map. In javascript, the idiom is T | null , so i guess my workaround is a valid idiom.

another way, a bit more verbose, is to use invariant library:

if (map.has(key)) {
  const a = map.get(key)
  invariant(a, "a is not available");
  a.whatever();
}

(flow nicely is able to figure out that a is not undefined/null after the invariant() instruction).

Thanks for working on this again.

This problem makes Flow incompatible with the technique of using WeakMaps to manage truly private data associated with ES6 class instances.

(The technique is discussed by Dr. Rauschmayer here and in more detail by Nicholas C. Zakas here.)

Currently I can't find any decent workaround.

Sometimes I'm doing manual memory management within a GCed language in JS. And when I do this, I know that .get will definitely return a non-undefined property. However needing to placate the flow types by checking the result is inefficient. Is there a way to override the flow type checker inference to make it believe me when I say I know that item will exist?

@CMCDragonkai

You can do something like that:

export default function memoize<A: *, R: *, K: mixed> (callback: (...args: A) => R, resolver: (...args: A) => K = (arg) => arg) {
    const cache: WeakMap<K, R> = new WeakMap();
    return function (...args: A): R {
        const key = resolver.apply(this, args);
        if (cache.has(key)) {
            return ((cache.get(key): any): R);
        }

        const result = callback.apply(this, args);
        cache.set(key, result);
        return result;
    };
};

I'm not too sure if this suits everyone, but the simplest solution is probably to change your code to this:

const prevVal: string | string[] = sectionBody.get(key);
if (prevVal) {
    sectionBody.set(key, [].concat(prevVal, value));
}

This is almost as fast as the original when the key is not present in the map and a bit faster when it is present. There is a tradeoff for sure, but at least Flow is ok with it and it doesn't add a lot of unreadable stuff around.

Thanks @cglacet - that's a clever workaround which works fine in my case.

I changed:

const map = Map();
function mapResult(key) {
  if (!map.has(key)) {
    map.set(key, getVal(key));
  }
  return map.get(key);
}

to:

const map = Map();
function mapResult(key) {
  let result = map.get(key);
  if (!result) {
    result = getVal(key);
    map.set(key, result);
  }
  return result;
}

(Typings omitted)

The work arounds above just not friendly for falsy values (麓-蠅喔乣)

my examples:

/* @flow */

const Settings = {
  REMEMBER_USERNAME: 'remember-username',
  REMEMBER_ME: 'remember-me'
}

const settings = new Map<string, boolean>()

settings.set(Settings.REMEMBER_USERNAME, true)
settings.set(Settings.REMEMBER_ME, false)

// Prefer these ways
function shouldRememberUsername (): boolean {
  return settings.has(Settings.REMEMBER_USERNAME)
    ? settings.get(Settings.REMEMBER_USERNAME)
    // Throws get(key: K): V | void;
    //                         ^ [1]
    : false // Assumes default to false
}

function shouldRememberMe (): boolean {
  return settings.has(Settings.REMEMBER_ME)
    ? settings.get(Settings.REMEMBER_ME)
    // Throws get(key: K): V | void;
    //                         ^ [1]
    : true // Assumes default to true
}

// Not these ways
function anotherShouldRememberUsername (): boolean {
  return settings.get(Settings.REMEMBER_USERNAME) || false
}

function anotherShouldRememberMe (): boolean {
  const rememberMe = settings.get(Settings.REMEMBER_ME)
  // Must to check for falsy values
  return rememberMe !== undefined
    ? rememberMe
    : true // Assumes default to true
}

Try Flow

Was this page helpful?
0 / 5 - 0 ratings

Related issues

xtinec picture xtinec  路  65Comments

opensrcery picture opensrcery  路  88Comments

sebmck picture sebmck  路  113Comments

MarcoPolo picture MarcoPolo  路  67Comments

NgoKnows picture NgoKnows  路  40Comments