Flow: Alternative to TypeScript's mapped types

Created on 22 Aug 2017  路  7Comments  路  Source: facebook/flow

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.

question

All 7 comments

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"})
Was this page helpful?
0 / 5 - 0 ratings

Related issues

ctrlplusb picture ctrlplusb  路  3Comments

mjj2000 picture mjj2000  路  3Comments

iamchenxin picture iamchenxin  路  3Comments

mmollaverdi picture mmollaverdi  路  3Comments

Beingbook picture Beingbook  路  3Comments