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

Ramda.assoc cannot be typed correctly #4748

Open Peeja opened 7 years ago

Peeja commented 7 years ago

Ramda.assoc(), by analogy with Clojure's assoc, takes a key, a value, and an object, and returns a copy of the object with that key and value added. (Ramda functions are also curried, but that's orthogonal to this issue.)

The most reasonable way to type assoc() I could come up with is the following, which doesn't work correctly:

declare function assoc<K: string, V, O>(key: K, val: V, src: O): O & { [K]: V };

let assocd = assoc('a', 'one', { b: 2 });
(assocd.a: string);
(assocd.b: number);
// This should be an error, but it's not, because the type of `assocd` now has
// an indexer property `{[string]: string}`.
(assocd.foo: string);

It appears that K becomes bound to string, and not the more specific type 'a'. Notice that constraining K to 'a' fixes the problem:

declare function assoc<K: 'a', V, O>(key: K, val: V, src: O): O & { [K]: V };

let assocd = assoc('a', 'one', { b: 2 });
(assocd.a: string);
(assocd.b: number);
// $ExpectError: This is now an error, because the type of `assocd` no longer
// uses an indexer property.
(assocd.foo: string);

I'm not certain where or if there's a shortcoming in Flow here, but it would be nice to be able to type assoc properly.

Peeja commented 7 years ago

Interestingly, a generic does appear to be able to get a literal type. For instance:

declare function foo <T>(a: T): {foo: T};
(foo('a'): {foo: 'a'});
// $ExpectError: `T` is bound to the literal type `'a'`, so the return value is known to be `{foo: 'a'}`.
(foo('a'): {foo: 'b'});

But, the other way around:

declare function bar <T>(a: T): {[T]: 'bar'};
(bar('a'): {a: 'bar'});
// This passes, but should fail. Flow does not require the property to be `'a'`.
(bar('a'): {b: 'bar'});

As before, bounding T to the literal 'a' makes it work for this specific case:

declare function bar <T: 'a'>(a: T): {[T]: 'bar'};
(bar('a'): {a: 'bar'});
// $ExpectError: The property is correctly required to be the literal `'a'`.
(bar('a'): {b: 'bar'});

So, while a generic can take a literal type, using it in an indexer property appears to force it to a primitive type, unless the generic is bounded so that that's not possible.