Flow: 'any' type infects key accesses

Created on 19 Aug 2016  路  12Comments  路  Source: facebook/flow

Example on FlowType Try

const a: {[key: string]: number} = {
  foo: 1,
};

const aStr: string = a['foo']; // errors correctly
const aKey: any = 'foo';
const bStr: string = a[aKey]; // should error, does not

Given an object of type {[key: string]: number}, indexing it with an any type shouldn't create a value of any. On the a object, for any given key, it should be treated as a ?number.

I assume this is a consequence of prototype chains (a['hasOwnProperty']: Function). Will the upcoming $Exact type fix this?

All 12 comments

But why ?number and not number?

For any given key, Flow should be able to say that it _could_ be a number, but it doesn't know for sure if the key is actually in the object. So it could be undefined.

I guess it should be void | number, otherwise getting a value is unsafe

const a: {[key: string]: number} = {}
const b: number = a.unknown // <= no error
console.log(2 * b) // => NaN

EDIT: NaN is indeed a number

another case

const a: {[key: string]: string} = {}
a.unknown.length // <= throws at runtime

That's interesting, the behavior is pretty odd - it actually assumes that _all_ keys are numbers, except if the key is from an Object type, then any flows.

FlowType Try

const a: {[key: string]: number} = {}
const b: string = a[String(Math.random())]; // errors
const obj: Object = {};
const c: string = a[obj.foo]; // should error, does not

I think that's by design, and won't change. Think of map {[key: string]: number} as a function (key: string) => number.

So, just to clarify the current behaviour:

arrays and dictionaries behave consistently if the key is not of type any

const arr: Array<number> = []
const elem: number = arr[0] // no error (unsafe?)
const elem: string = arr[0] // error

const dict: {[key: string]: number} = {}
const val: number = dict['foo'] // no error (unsafe?)
const val: string = dict['foo'] // error

while they behave differently if the key is of type any

const arr: Array<number> = []
const i: any = 0
const elem: string = arr[i] // <= error: number. This type is incompatible with string

const dict: {[key: string]: number} = {}
const k: any = 'foo'
const val: string = dict[k] // no error <= this seems a bug

@gcanti well, that part is certainly a bug

Thanks for isolating it @gcanti

This isn't actually specific to any. We currently allow any statically unknown strings or numbers to index any object. This is unsound and I'd like to tighten things up here, but we have this rule because many common JS idioms run into this.

Example:

declare var obj: { p: number };
declare var strkey: string;
declare var numkey: number;
(obj[strkey] : number & string);
(obj[numkey] : number & string);

Note that number & string is a cheap way of constructing an "empty" type, as no type other than any (or itself) can inhabit that type.

So, long story short, this is working as designed. You may be interested to read the source in the type checker that allows this.

@samwgoldman I think it only supposed to work like this for record objects, not for map objects

OK, I see. I missed the dictionary part. Right above the code I linked to shows the logic for the dictionary case, but we never reach it because we don't propagate the any there. This should be a simple fix, but to be clear, this is the expected behavior?

const dict: {[key: string]: number} = {}
const k: any = 'foo'
const val: string = dict[k] // error: number incompatible with string

Right, that's the behavior I would expect.

Was this page helpful?
0 / 5 - 0 ratings