My high level goal is to implement type definitions for the immutability library which supports updating objects 脿 la update({a: { b: true}}, {a: {b: {$set: false}}).
TypeScript provides mapped types which look very useful for this task.
However, I couldn't get an alternative implementation for flow to work. I tried two different approaches with $ElementType and with $ObjMap.
Both failed due to too weak type checking.
type SetterShape<T, K: $Keys<T>> = {
[key: K]: $ElementType<T, K> | { $set: SetterShape<$ElementType<T, K>, $Keys<K>> }
}
declare function test<T, K>(obj: T, SetterShape<T, K>): void;
declare function keyTransform<T, K>(key: K): K | {
$set: $ElementType<T, K> // cannot refer SetterShapeObjMap since it's not defined yet
}
type SetterShapeObjMap<T> = $ObjMap<T, typeof keyTransform>;
declare function testObjMap<T, K>(obj: T, SetterShapeObjMap<T>): void;
const obj = {
d: true
};
test(obj, {d: {$set: false}}); // correctly typechecks
test(obj, {d: {$set: null}}); // incorrectly typechecks
test(obj, {d: null}); // incorrectly typechecks
// test(obj, {e: null}); // correctly errors
testObjMap(obj, {d: {$set: false}}); // correctly typechecks
testObjMap(obj, {d: {$set: null}}); // incorrectly typechecks
testObjMap(obj, {d: null}); // incorrectly typechecks
// testObjMap(obj, {e: null}); // correctly errors
Is such a type definition possible with flow? Any help would be appreciated.
I'm not sure how to do something exactly like this, but I think I can see that the approach you're currently taking isn't going to work, for a reason that's in addition to the problem you're already having here.
The problem creeps up once the object being changed has multiple properties with different types. Here's a stripped-down example:
const obj = {
d: true,
c: "stuff"
};
let x: $ElementType<typeof obj, $Keys<typeof obj>> = true; // type error!
There's actually no value you can put in place of true that will type-check here. If you use declarations to play around with what type is expected, this is how to make it type-check:
declare var x: bool & string;
let z: $ElementType<typeof obj, $Keys<typeof obj>> = x;
In other words, the value needs to be both a bool and a string to type check there! That's not going to work...
The problem is that in the $ElementType<T, K> where K is $Keys<T>, K isn't standing for one particular key, but for the set of all possible keys in the object. As a result, $ElementType<T, K> returns a type not for one specific key, but that represents the intersection of the types for all the keys.
I can't think of a way to say what I think you want to say, which is, "given an object shape, create a new object shape that allows the same keys and a new type which is the application of a type constructor to the type of that key in the original object".
A related problems happens with $ObjMap. If the object has several keys and you only want to set one of them, the type created by $ObjMap will not allow that because it expects all the original keys to be present.
(FWIW, I know this doesn't help in your project, but I have found that trying to type these helper functions that come from untyped libraries is a ton of effort that often doesn't pay off. I think it can sometimes be worthwhile to start from scratch with a type-centric way of doing generic data access/mutation. For that, lenses are a great abstraction. I like the safety-lens library, which has solid Flow types. But it probably requires learning about the abstract idea of lenses before diving into that library.)
@asolove Thanks for your answer. The problem you are pointing out is comprehensible, but I think TypeScript got it right. One example from their documentation is:
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
}
This code concisely relates each key of an object to its type, without requiring that the values have all the same type.
Flow's $ObjMap seems to be able to deal with differing types of object values in this example from the documentation:
// @flow
// let's write a typelevel function that takes a `() => V` and returns a `V` (its return type)
type ExtractReturnType = <V>(() => V) => V
function run<A, O: {[key: string]: () => A}>(o: O): $ObjMap<O, ExtractReturnType> {
return Object.keys(o).reduce((acc, k) => Object.assign(acc, { [k]: o[k]() }), {});
}
const o = {
a: () => true,
b: () => 'foo'
};
(run(o).a: boolean); // Ok
(run(o).b: string); // Ok
However, this seems to only work if the $ObjMap is used as a return type (in contrast to a parameter of the function).
FWIW, I know this doesn't help in your project, but I have found that trying to type these helper functions that come from untyped libraries is a ton of effort that often doesn't pay off.
I'd hope that flows type system is expressive enough, but I'll take a look at your safety-lens recommendation. Thank you!
Safety lens has a little problem. It doesn't introduce inferrable types. For example this code won't fail
// @flow
import { prop } from 'safety-lens/es2015';
const nameLens = prop('name');
const r1: number = nameLens.get({
name: '1'
});
nameLens.set(1, {
name: '1'
});
Just this one example makes this lib useless.
I certainly understand that downside, but I wouldn't go as far as "useless". I've used it on a largish project and quite enjoyed it. There certainly is a downside where you have to explicitly type the lenses or risk not having coverage. But on a largish project where those lenses get exported and used in other code, you'd have to add the types anyways. And the result is nicely composable, well-typed code. So I find it preferable to using one of the libdefs for untyped libraries like ramda or lodash. But, like I said, definitely a tradeoff.
In any event, we're now quite far afield from the original question, so I'll voluntarily bow out of any further debate about this.
how can I convert this types from mapped conditional types https://gist.github.com/gtkatakura/39debf900c6b4f78bae40ccc3bab6d42
to flow $ObjMap?
Continuation to this,
How can this not fail?
type Row = {|
val: string
|}
type Cart<T> = $ObjMap<T, <V>(a:V)=>Array<V>>
const f = (a: Row):Cart<Row> =>{
return {
val: [3,"foo"]
}
}
f({val:"hello"})