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?
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.
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.