Is flow's information loss associated with structural subtyping inherent to this kind of polymorphism?

178 views Asked by At

Structural subtyping in flow may lead to information loss:

type O = {x: number, y: number};
type P = {x: number, y: number, z: number}

function f(o: O) {
    return o.x * 2, o.y * 2, o;
}

const p: P = {x: 2, y: 3, z: 100};
const r = f(p);

r.z // type error (property not found)

(This code is terrible, because it performs visible mutations. It is for illustrative purposes only.)

I've read that row polymorphism is a concept to avoid this information loss without jeopardizing type safety.

Is there a way to achieve the same with subtype polymorphism?

[EDIT]

To address a greater audience I provide a brief explanation of the somewhat scary terminology:

  • Polymorpishm is just a fancy word for determining if two types are equivalent, i.e. it makes a rigid type system more flexible
  • Parametric polymorphism (generics in flow) states that two types are always equivalent, because types doesn't matter at all
  • Subtype polymorphism (subtyping in flow) states that two types are equivalent if you can derive a hierarchy from them, i.e. subsume the subtype under its supertype
  • Row polymorphism is similar to subtyping but solves the information loss issue (technically, however, there is no subtype relationship anymore, so it isn't a form of subtyping)
  • Bounded polymorphism states that two types are equivalent for a specific purpose only, e.g. equality, order, mapping-over etc.
1

There are 1 answers

5
kyle On

For what I have read about Flowtype I am pretty sure that your function is the problem.

If you do this instead:

function f<T: O>(o: T): T {
  o.x *= 2;
  o.y *= 2;
  return o;
}

r.z; // okay

This works because of bounded polymorphism. Now the body type checks under the assumption that T is a subtype of O. Furthermore, no information is lost across call sites. Read more about it here.

Also, I hadn't heard of row polymorphism before, so I went and looked it up. While looking it up I have read several things that seem to indicate that row polymorphism isn't subtyping. 1, 2, 3.

To expand on this answer and to clarify why the OPs function doesn't work but the one that I proposed will work correctly. This is a nice reference as well but is specific to Java.

By having the function as:

function f(o: O) {
  return o.x * 2, o.y * 2, o;
}

The function specifies that it is explicitly looking for an object of type O, rather than an object of O or a subtype of O is allowed. In the OPs function we are downcasting the parameter o to type O, rather than using a generic (this is bad). The proper way to handle this is utilizing generics to specify that it can be type O or subtype of O, which can be done as follows:

function f<T: O> (o: T): T {
  o.x *= 2;
  o.y *= 2;
  return o;
}

Check out the docs on how flow handles generics and how it is related to function parameters, object, etc..

The relevant part is:

  • "Generics allow you to hold onto the more specific type while adding a constraint. In this way types on generics act as “bounds”. link

  • "Generics sometimes allow you to pass types in like arguments to a function. These are known as parameterized generics (or parametric polymorphism)." link