How do I return the same type that is passed into a function? - flowtype

I have a function which can receive a value of type either TLocation or {height: number, width: number}.
TLocation is defined as follows:
type TLocation = {
height: number,
width: number,
x: number,
y: number
}
Here is my function:
function transformer<
L: { height: number, width: number } | TLocation,
V: { height: number, width: number }
>(location: L, viewbox: V): L {
if (location.hasOwnProperty('height')) {
return {
height: location.height / viewbox.height,
width: location.width / viewbox.width,
x: location.x / viewbox.width,
y: location.y / viewbox.height
};
} else {
return {
height: location.height / viewbox.height,
width: location.width / viewbox.width
};
}
}
Here is a screenshot of where I have flow errors:
Both errors are:
Flow: Cannot return object literal because object literal [1] is incompatible with `L` [2].
I know I can't use hasOwnProperty as I am trying to do, but I'm not sure how to properly return the correct value (and therefore type).
If there is anything special I need to do when calling the function, I'd really appreciate an example of the usage as well.

The short answer is that no, you can't do this with flow, not in the way you're trying to.
Fundamentally, you have two different main things going on. You have a type refinement problem, and you have a problem with returning a generic type.
Type Refinement
Type refinement is an area where the official flow docs are really insufficient. It would be a lot more useful if they were just clear about what can't be accomplished with type refinement currently. It's really pretty simplistic.
You basically have two object types, A and B, but you know that A has a member B doesn't, so you should be able to refine the object to A or B based on whether or not that member exists. It makes sense to you, and it seems like it should make sense to flow. But flow doesn't implement a mechanism for that.
type A = {| +aMember: string |};
type B = {| +bMember: string |};
// `thing` is a union of `A` and `B` who have different properties
declare var thing: A | B;
// Only `A` has `aMember`, so we should be able to check for that
// to refine the type, right?
if (typeof thing.aMember === 'string') {
(thing: A); // ERROR! `thing` is still a union of `A` and `B`!
}
(try)
Unfortunately type refinement in flow is a bit too naive to figure this out. What this particular check does accomplish is that it refines the type of the property:
if (typeof thing.aMember === 'string') {
// we can now use `thing.aMember` as a string just fine, but
// `thing` is still a union of `A` and `B`
(thing.aMember: string);
}
(try)
The only way that we can refine whole objects into different types in flow is with a disjoint union. This would mean that we would introduce a literal field to distinguish between the two object types, which is probably not what you want to do in this case:
type A = {| type: 'a', +aMember: string |};
type B = {| type: 'b', +bMember: string |};
// `thing` is a union of `A` and `B` who have different properties
declare var thing: A | B;
if (thing.type === 'a') {
// flow now knows that `thing` is of type `A`
(thing.aMember: string);
} else {
// and here in the else, flow knows that `thing` is of type `B`
(thing.bMember: string);
}
(try)
Generic Return Type
So what happens if we decide to use a disjoint union to resolve this?
type Common = {| width: number, height: number |};
type Size = {| ...Common, type: 'size' |};
type Rect = {| ...Common, type: 'rect', x: number, y: number |};
const func = <T: Size | Rect>(thing: T): T => {
// Here we've bypassed the type refinement problem by using a
// disjoint union, but we're not returning `thing`, are we?
if (thing.type === 'size') {
// ERROR!
// Cannot return object literal because object literal [1]
// is incompatible with `T` [2].
return { type: 'size', width: 0, height: 0};
} else {
// ERROR!
// Cannot return object literal because object literal [1]
// is incompatible with `T` [2].
return {
type: 'rect',
width: 0,
height: 0,
x: 0,
y: 0,
}
}
}
(try)
The problem is that our function expects to return exactly T, but we're not returning T, we're returning some other type. You might think, "we're gonna return something of a type that T is compatible with at the time it gets returned," but that's not actually what we're telling flow. We're telling flow to expect us to return exactly T.
Let's look at an even simpler example:
const f = <T: number | string>(thing: T): T => {
if (typeof thing === 'number') {
return 0;
// ERROR!
// Cannot return `0` because number [1] is incompatible
// with `T` [2].
} else {
return 'stuff';
// ERROR!
// Cannot return `stuff` because string [1] is incompatible
// with `T` [2].
}
}
(try)
Why does this not work? Well, I think that the best mental model to use to consider this, is templates. Let's consider that the way that generic types work is by creating versions of the function for each member of the set that the generic type bounds define. This obviously isn't how this actually works, but it'll be useful when considering this example.
Our generic in this case defines a set of two types, string and number, so let's consider that this results in two versions of this function:
const fForNumber = (thing: number): number => {
if (typeof thing === 'number') {
return 0;
// Works fine.
} else {
return 'stuff';
// ERROR!
// Cannot return `stuff` because string [1] is incompatible
// with `number` [2].
}
}
const fForString = (thing: string): string => {
if (typeof thing === 'number') {
return 0;
// Cannot return `0` because number [1] is incompatible
// with string [2].
} else {
return 'stuff';
// Works fine.
}
}
(try)
So basically we've just copied and pasted the function, then replaced T with number in the case of the first function, and we've replaced it with string in the second case. It's pretty clear when looking at each of these versions independently why they would have errors. Now we just need to consider that the version with the generic is basically these two functions superimposed on top of each other, and the result is that we get both errors. To not get any errors in our generic functions, every type defined by the bounds of the generic must independently make sense in place of the generic through every path of the function.
Solutions
Given this formulation, the most immediate solution is to just ignore the errors basically:
type TLocation = {
height: number,
width: number,
x: number,
y: number
}
function transformer<
L: { height: number, width: number } | TLocation,
V: { height: number, width: number }
>(location: L, viewbox: V): L {
// Refine both of these properties so we can use them without errors:
if (typeof location.x === 'number' && typeof location.y === 'number') {
return (({
height: location.height / viewbox.height,
width: location.width / viewbox.width,
x: location.x / viewbox.width,
y: location.y / viewbox.height
// Type through `any` to `L`
}: any): L);
} else {
return (({
height: location.height / viewbox.height,
width: location.width / viewbox.width
// Type through `any` to `L`
}: any): L);
}
}
This is sort of okay because we (the authors, not flow) know what the type we should be returning is, so as long as we are returning that correctly it should work fine. But obviously this is sort of ugly since we're basically just turning off the type system. Unfortunately we're gonna have to make a compromise somewhere to make this particular formulation work.
Usually in this type of situation it makes the most sense to back up and implement a more explicit design:
type Size = {| width: number, height: number |};
type Rect = {| ...Size, x: number, y: number |};
function sizeTransformer(size: Size, viewbox: Size): Size {
return {
height: size.height / viewbox.height,
width: size.width / viewbox.width,
}
}
function rectTransformer(rect: Rect, viewbox: Size): Rect {
return {
...sizeTransformer({ width: rect.width, height: rect.height }, viewbox),
x: rect.x / viewbox.width,
y: rect.y / viewbox.height
}
}
(try)
Obviously you have to consider how this is currently being used, but usually there is a way to implement a more specific solution.
I think that one thing people tend to do when they start working with strongly typed code is to try and fit strong typing onto patterns they're used to from more dynamic environments. In my experience, this is often a code smell. It's often revealing that the code could be considered more from a data-first perspective. Generics are a big and powerful tool, but if you're asking yourself whether or not you can use them to solve a really small use case, then it might be a good time to step back and reconsider the approach.

Related

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

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

Flow union type with exact object types gives 'property is missing in [type 1]'

I have a function that accepts a union two disjoint exact types, and constructs a new object. The react documentation indicates that with disjoint exact types, I should be able to infer the type by checking for the existence of a property. However, this doesn't appear to be working -- my else branch indicates that the properties are missing on one of the types.
type Sid = {|
val: string,
id: number
|};
type Entity ={
idOrName: Sid | string,
optionalDescription: ?string
}
const createEntity = (idOrNameAndDescription: Sid | {| name: string, description: string |}): Entity => {
if (idOrNameAndDescription.val) {
return {
idOrName: idOrNameAndDescription,
optionalDescription: null
}
} else {
return {
// These next to lines fail, saying:
// "Flow: Cannot get `idOrNameAndDescription.name` because
// property `name` is missing in `Sid`
idOrName: idOrNameAndDescription.name,
optionalDescription: idOrNameAndDescription.description
}
}
}
See the comment in the else/return statement. The errors given are:
Cannot get idOrNameAndDescription.name because property name is missing in Sid [1].
[1] 60│ const createEntity = (idOrNameAndDescription: Sid | {| name: string, description: string |}): Entity => {
:
65│ }
66│ } else {
67│ return {
68│ idOrName: idOrNameAndDescription.name,
69│ optionalDescription: idOrNameAndDescription.description
70│ }
71│ }
Cannot get idOrNameAndDescription.description because property description is missing in Sid [1].
[1] 60│ const createEntity = (idOrNameAndDescription: Sid | {| name: string, description: string |}): Entity => {
:
66│ } else {
67│ return {
68│ idOrName: idOrNameAndDescription.name,
69│ optionalDescription: idOrNameAndDescription.description
70│ }
71│ }
72│ }
However, as .val was not defined flow should know that it is of the other half of the union and name and description are defined.
I've also tried using if (idOrNameAndDescription.val != null), which makes both of the branches of the if statement invalid as well as if (typeof idOrNameAndDescription.val === 'string'), which results in an even uglier error.
Is this a flow bug or am I missing something?

Optional group of keys

I have to make two keys optional index and remove in an object. However if one is provided the other must be there. So its like this:
type Props = {
isSettings: boolean,
} | {
index: number,
remove: $PropertyType<FieldProps, 'remove'> // (index: number) => void
}
Where the second object is optional. The above is not working, as it is not expecting isSettings in the 3rd object. But it is always required.
Standard object types in Flowtype are objects defined to have at least the properties specified. This means that if you have a type like { isSettings: boolean } you are saying only that that the object has an isSettings property that is a boolean. It is allowed to have other properties, it just has to know the type of isSettings.
This means that if you have a type
type Props = {
isSettings: boolean,
} | {
index: number,
remove: (index: number) => void
};
then doing
var obj: Props = ...
if (obj.remove) {
var n: number = obj.index;
}
will fail, because it doesn't prove anything, because you have not prohibited there being a remove property on both objects.
In order to refine an object type like this one, Flow needs to be told that each type has exactly the given set of properties. This is where Flow's Exact object types come in.
If you change your types to be
type Props = {|
isSettings: boolean,
|} | {|
index: number,
remove: (index: number) => void
|};
then a snippet like
var obj: Props = ...
if (obj.remove) {
var n: number = obj.index;
}
will work as expected, because the presence of remove means there must be a property called index that is a number.

Why doesn't Flow see members of objects in this polymorphic union type?

The Setup
Here's a complete try flow example illustrating the issue.
Types
export type ActionT<TT: string, PT> = {|
+type: TT,
+payload: PT,
+error?: boolean,
+meta?: any
|}
export type ChangePayloadT = {
+_change: {|
+state: 'PENDING' | 'FULFILLED' | 'REJECTED',
+id: string,
+error?: any,
+message?: string,
|}
}
export type IdPayloadT = {
id: string,
}
type PayloadT = IdPayloadT | ChangePayloadT
type MyActionT = ActionT<'SET' | 'MERGE', PayloadT>
As you can see, MyActionT can contain a payload with either an id or a _change object. It's not quite (?) a disjoint union because there isn't a single property to disambiguate on.
This seems like it should work, but doesn't:
function lookup3 (action: MyActionT): any {
if (action.payload._change) {
// why does this error?
return action.payload._change.id
} else {
return action.payload.id
}
}
Anyone care to set me straight as to why?
Ok, so the solution apparently involved making the the two types a proper disjoint union:
export type ChangePayloadT = {
+_change: {|
+state: $Keys<typeof asyncStates>,
+id: string,
+error?: any,
+message?: string,
|},
id?: string,
}
export type IdPayloadT = {
+_change?: void,
+id: string,
}
With the second type now having an explicitly void _change, flow knows to tell the types apart based on the presence or absence of a _change.
Working tryflow Yay. :)

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