Flow type a generic function that groups any type that has at least an ID - flowtype

I'm trying to type with Flow a function that maps As to Bs, where the only restrictions are:
B contains an A
A has at least an id property which is a string
Apart from that, A can be any object, and, in this situation, B is well known.
I want to type the function with a generic/polymorphic type that so the type checker knows that you will get an array of objects containing the A and B that matches.
My attempt below does not give me any type error, but I don't think it is correct either.
Will love to understand how to properly type this so you can get the most guarantees.
type B = {A: {id: string}}
const BContainsA = (id: string) => (b: B) =>
b.A.id === id
type MapResult<T> = {
AsWithBs: Array<{ A: T, B: B }>,
AsWithoutBs: string[],
}
const mapAsToBs = <T>(
As: { ...T, id: string }[],
Bs: B[]
): MapResult<T> => {
return As.reduce(
(result, a) => {
const b = Bs.find(BContainsA(a.id))
if (!b) {
result.AsWithoutBs.push(a.id)
return result
}
result.AsWithBs.push({ A: a, B: b })
return result
},
{ AsWithBs: [], AsWithoutBs: [] }
)
}
mapAsToBs([{pos:2,id: '1'},{pos:1,id: '11'}],[{A:{id: '1'}}])

Seems that all I had to do was to add a constraint to the generic type like this:
const mapAsToBs = <T:{id: string}>(
As: T[],
Bs: B[]
): MapResult<T> => {
}
It is indeed documented, but the syntax is so unintuitive and the explanation is so short, that I would have never guessed it just by reading it.
You can check how it works as expected here

Related

Is there a better way to compose this in a point-free way?

I come across this use-case at work all the time and I feel like there must be a way to compose fullName in a point-free way without defining cat as a parameter:
const cats = [
{ name: 'Bob', lastName: 'Ross' },
{ name: 'Frank', lastName: 'Langella' },
];
// this bugs me
const fullName = cat => add(
prop('name', cat),
prop('lastName', cat)
);
const isEqual = curry((a, b) => a === b);
const isBobRoss = compose(isEqual('BobRoss'), fullName);
edit: some of the helpers above in case it helps understand the challenge
/**
* compose :: ((a -> b), (b -> c), ..., (y -> z)) -> a -> z
*/
const compose = (...fns) => (...args) =>
fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];
/**
* curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c
*/
function curry(fn) {
const arity = fn.length;
return function $curry(...args) {
if (args.length < arity) {
return $curry.bind(null, ...args);
}
return fn.call(null, ...args);
};
/**
* add :: a -> b -> a + b
*/
const add = curry((x, y) => x + y)
/**
* prop :: String -> Object -> a
*/
const prop = curry((p, obj) => obj[p])
}
This is no exactly function composition, but sure you can write a helper function for it. Ramda does know it as converge, here's a simplified (unary) version of it:
const converge = (fn, wraps) => arg => fn(...wraps.map(wrap => wrap(arg)));
cost fullName = converge(add, [prop('name'), prop('lastName')]);
(Initially I decline to commit to a specific functional programming library for JS/TS and work in general concepts)
Notice that your curred prop function takes a key and returns a reader monad. This will be useful.
Assuming type Obj = {name:string,lastName:string}, then your curried prop fn is (key:'name'|'lastName') => Reader<Obj,string>
You can use a sequence type function to combine two reader monads into a single one, as such:
const getNameParts = sequence(prop('name'), prop('lastName')) // Reader<Obj, [string,string]>
Then you can map the [string,string] to be a single string like in your add function
const add = as => as.reduce((acc, item) => acc + item)
So if you can lift add into your reader monad's computational context (here using a proposed `map: ((a:A)=>B)=>(fa:F<A>)=>F<B>), then compose these operations:
const buildNameFromObject = compose(getNameParts, map(add)) // Reader<Obj, string>
There we have it.
const personsName = buildNameFromObject(someObject) // string
fp-ts is a library that provides everything I just mentioned, and using that library (with a few function name changes to align with fp-ts's vocabulary),
import { reader, map } from 'fp-ts/lib/Reader'
import { sequenceT } from 'fp-ts/lib/Apply'
import { pipe } from 'fp-ts/lib/pipeable'
const getNameParts = sequenceT(reader)(prop('name'), prop('lastName'))
const buildNameFromObject = pipe(getNameParts, map(add)) // Reader<Obj, string>, which is a fancy way of writing (o:Obj)=>string
buildNameFromObject({name:'Foo', lastName: 'Bar'}) // 'FooBar'
Your "fullName" function (buildNameFromObject) is now point free.

flow returns wrong type

I have this piece of code:
/* #flow */
import { List } from 'immutable';
type NotMapped = {|
first: Array<number>,
second: number,
|};
type Mapped = {|
first: List<number>,
second: number,
|};
const notMapped: NotMapped = {
first: [1, 2, 3],
second: 10,
};
const map = () => {
const first = List(notMapped.first);
return { ...notMapped, first };
}
const result: Mapped = map();
I want result to beMapped type but I get:
{|
first: List<number> | Array<number>,
second: number
|}
How come that flow thinks it might be Array<number> when I explicitly set first as List(notMapped.first)? It only works if I set return { second: notMapped.second, first };, but this isn't a solution because I have large amount of data and I cannot set every item.
I have checked and this is well-known issue, but when spreading types, not object with assigned type. Is there any solution for this?

FlowType errors using Object.entries

So, I have the following code, but flow errors keep popping up. I've tried to cast the Object.entries, but just won't work - others things to. Any insight?
type Fields = {
name: string,
func: (*) => boolean
};
type S = {
key1: Fields,
bill: Fields
}
var a: S = {
key1: {name: 'mary', func: (str) => str === 'mary'},
bill: {name: 'bill', func: (str) => str === 'bill'}
}
var c = Object
.entries(a)
.map(([key, obj]) => obj.func(key) ? obj : false)
.filter(f => f)
.reduce((acc, c) => {
return 'something here'
}, {});
I've left some things off, but the slow is the same. Flow is reading that entries as a return Tuple Type. I've tried all sorts of things, but instead of mudding things up, I left it untouched.
I can't seem to annotate the destructured items here ([key, obj]), get tuple errors...
Any assistance on getting that code assigned to var c, to work with annotations etc..?
The errors I get:
Cannot call method on mixed type (from obj.func)
Cannot assign value in Tuple etc..
The error is accurate. Object.entries has the type
entries(object: any): Array<[string, mixed]>;
It has no way to know what the type of the second item in the tuple will be. That means your code
.map(([key, obj]) => obj.func(key) ? obj : false)
would need to do
.map(([key, obj]) => {
if (typeof obj.func !== 'function') throw new Error();
return obj.func(key) ? obj : false;
})
so that flow knows that it is guaranteed to be a function.
Alternatively, you could change your data structure to use a type where the second item in the tuple has a guaranteed type, like Map, e.g.
type Fields = {
name: string,
func: (string) => boolean
};
type S = Map<string, Fields>;
var a: S = new Map([
['key1', {name: 'mary', func: (str) => str === 'mary'}],
['bill', {name: 'bill', func: (str) => str === 'bill'}],
]);
var c = Array.from(a, ([key, obj]) => obj.func(key) ? obj : false)
.filter(f => f)
.reduce((acc, c) => {
return 'something here'
}, {});
In my case, I had:
let objectsByName : { [string] : MyObjectType } = {}; //simple map
...
objectsByName[object.name] = object; //call repeatedly to populate map.
...
let results : any[] = []; //next we will populate this
Trying to operate on it like this failed for Flow (though this is executable JavaScript):
for (let [name : string, object : MyObjectType] of Object.entries(objectsByName))
{
let result = doSomethingWith(object); //<- error on arg
results.push(result);
}
This succeeded for Flow:
for (let name : string in objectsByName)
{
let object = objectsByName[name];
let result = doSomethingWith(object); //<- error on arg
results.push(result);
}
It is annoying having to change code structure to suit a supposedly non-intrusive system like Flow comment types, which I chose in the hopes of making my code completely oblivious to Flow's presence. In this case I have to make an exception and structure my code as Flow wants it.
Replacing Object.entries with Object.keys + lookup fixes flow errors for me assuming the input object is properly typed.
i.e. replace Object.entries(a) with Object.keys(a).map(key => [key, a[key]])
This works with flow:
type Fields = {
name: string,
func: (*) => boolean
};
type S = {
key1: Fields,
bill: Fields
}
var a: S = {
key1: {name: 'mary', func: (str) => str === 'mary'},
bill: {name: 'bill', func: (str) => str === 'bill'}
}
var c = Object
.keys(a)
.map(key => a[key].func(key) ? obj : false)
.filter(f => f)
.reduce((acc, c) => {
return 'something here'
}, {});

How to get a function's return type in flow?

With this example:
const myObj = {
test: true,
};
type MyType = typeof myObj;
const getValue = (): MyType => {
return myObj;
};
// how to do this??
type TheReturnType = getValue;
const nextObj: TheReturnType = {
test: false,
};
I'd like to extract the type that the function will return, so I can reuse that type. I can think of no way to get it. The above doesn't work. typeof getValue will return the function.
Flow has a $Call utility type, which can get a function's return type:
type TheReturnType = $Call<typeof getValue>
However, if your function takes arguments, you need to provide types for those as well:
type TimeoutType = $Call<typeof setTimeout, () => void, number>
If that seems inconvenient, you can write a ReturnType helper that can skip the need for arguments:
type ReturnType<F> =
$PropertyType<$ObjMap<{ x: F }, <R>(f: (...any) => R) => R>, 'x'>
Let's use this:
type TheReturnType = ReturnType<typeof setTimeout>
This ReturnType helper basically matches the ReturnType helper present in TypeScript.

How to flowtype cover this code in a function with dereferenced object fields

I'm new to flow, any trying to cover some of my functions, however often I have these snippets where I extract fields form an object based on some condition. But I'm struggling to cover them with flow.
const _join = function ( that: Array<Object>, by: string, index: number) {
that.forEach((thatOBJ: {[string]: any}, i: number)=>{
let obj: {[string]: any} = {};
for (let field: string in thatOBJ) {
if (field !== by) {
obj[`${index.toString()}_${field}`] = thatOBJ[field]; // NOT COVERED
} else {
obj[field] = thatOBJ[field]; // NOT COVERED
}
that[i] = obj;
}
});
}
The array that in this code is a data array so can really be in any format of mongodb data.
Any ideas on what to add to make the two lines which are not covered by flow covered?
Thanks.
A few notes...
This function has a "side effect" since you're mutating that rather than using a transformation and returning a new object.
Array<Object> is an Array of any, bounded by {}. There are no other guarantees.
If you care about modeling this functionality and statically typing them, you need to use unions (or |) to enumerate all the value possibilities.
It's not currently possible to model computed map keys in flow.
This is how I'd re-write your join function:
// #flow
function createIndexObject<T>(obj: { [string]: T }, by: string, index: number): { [string]: T } {
return Object.keys(obj).reduce((newObj, key) => {
if (key !== by) {
newObj[`${index}_${key}`] = newObj[key]
} else {
newObj[key] = obj[key]
}
return newObj
}, {})
}
// NO ERROR
const test1: { [string]: string | number } = createIndexObject({ foo: '', bar: 3 }, 'foo', 1)
// ERROR
const test2: { [string]: string | boolean } = createIndexObject({ foo: '', bar: 3 }, 'foo', 1)

Resources