facebook / flow

Adds static typing to JavaScript to improve developer productivity and code quality.
https://flow.org/
MIT License
22.09k stars 1.86k forks source link

Flow does incorrect type refinements for inferred polymorphic instances #4371

Closed Gozala closed 1 year ago

Gozala commented 7 years ago

Here is an example that illustrates the issue:

/* @flow */

type Dict <a> = {[key:string]:?a}

const set = <a> (key:string, value:a, dict:Dict<a>):Dict<a> => {
  dict[key] = value;
  return dict
}

const get = <a> (key:string, dict:Dict<a>, fallback:a):a => {
 const value = dict[key]
 if (value != null) {
   return value
 } else {
   return fallback
 }
}

const d1 = set("a1", "Jack", {}) // I'd expect d1:Dict<string>
const v = get("a1", d1, 0) // I'd expect v:string|number
v.toFixed() // but flow inferst as v:number

From what I can tell second call to get ignoring the fact that d1 is Dict<string> or more likely set returns Dict<any> and there for v is inferred as number, completely ignoring a fact that set stored a string there.

Gozala commented 7 years ago

It seems that Dict type alias causes misbehavior, if I inline it it works as expected:

/* @flow */

const set = <a> (key:string, value:a, dict:{[key:string]:?a}):{[key:string]:?a} => {
  dict[key] = value;
  return dict
}

const get = <a> (key:string, dict:{[key:string]:?a}, fallback:a):a => {
 const value = dict[key]
 if (value != null) {
   return value
 } else {
   return fallback
 }
}

const d1 = set("a1", "Jack", {}) // I'd expect d1:Dict<string>
const v = get("a1", d1, 0) // I'd expect v:string|number
v.toFixed() // but flow inferst as v:number

and reports:

    21: v.toFixed() // but flow inferst as v:number
          ^ property `toFixed`. Property not found in
    21: v.toFixed() // but flow inferst as v:number
        ^ String
Gozala commented 7 years ago

It seems that Dict type alias throws away what it knows about the property value type, and you could use a hack along this lines to stop it from throwing it away:

/* @flow */

type Dict <a> = {
  [string]:?a,
  value?: a
}

const set = <a> (key:string, value:a, dict:Dict<a>):Dict<a> => {
  dict[key] = value;
  return dict
}

const get = <a> (key:string, dict:Dict<a>, fallback:a):a => {
 const value = dict[key]
 if (value != null) {
   return value
 } else {
   return fallback
 }
}

const d1 = set("a1", "Jack", {}) // I'd expect d1:Dict<string>
const v = get("a1", d1, 0) // I'd expect v:string|number
v.toFixed()
/*
  ^ property `toFixed`. Property not found in
^ String
*/
SamChou19815 commented 1 year ago

This should be fixed now