Sound Typing for “this” in Flow
In our most recent post, we alluded to some upcoming changes that would restrict some common JavaScript features. In this post, we will outline the first few of these changes, which you can expect to see in upcoming releases, and which are designed to address one of Flow’s longest-standing unsoundnesses: typing the this
parameter.
The this
parameter of functions and methods has historically been one of Flow's major blind spots; we were able to infer a type for this
when functions were used within a file, but had to resort to inferring any
for any exported functions. Class methods assumed that methods were called on an instance of the class, but this was not enforced. So the following code would not generate any errors in Flow.
class C {
foo() {}
bar() { this.foo() }
}
var bar = new C().bar;
bar(); // runtime error!
Similarly, in the below code, Flow would allow you to call the exported function from other files with any this
argument, and you were unable to provide an annotation for the parameter.
export function foo() : number {
return this.bar;
}
To address these issues, Flow is adding support for annotating this
parameters of functions and methods, as well as restricting and modifying the way that this
interacts with some features of classes, class methods, and objects. To provide sound and simple behavior for this new feature, Flow will error when class methods are unbound and when this
is used inside object methods. Flow is also restricting the ways that class instances interact with object types. The behavior of the this
parameter is commonly a source of confusion, and JavaScript itself has evolved away from using it by introducing new language features like arrow functions and classes that discourage its use. By eliminating outdated, complex language features and instead focusing on modeling modern idioms, we can make Flow a simpler, safer and friendlier language.
However, this is a large set of changes that modify some pretty important parts of Flow’s type system, so updating your codebase to be compatible with these improvements may be challenging, and may take some time. We have released some tools (which will be detailed below) that will help with these improvements, but we encourage upgrading to the newest version of Flow and suppressing (and then fixing over time) the new errors, rather than attempting to fix all the errors ahead of time. That all said, we think the benefits are very worth it, so let’s dive into the specifics:
"this”
parameter annotations
Inspired by TypeScript, we will be adding support for this
annotations in a special position at the beginning of a function's parameter list. When present, the this
parameter must always have an annotation, and must always appear first in the list. The following are valid examples of this annotations:
function foo(this: { x: string }) {
/* code */
}interface I {
m(this: I): void;
}class A {
m(this: I): void {
/* code */
}
}
while the following are not:
function bar(x: number, this: string) {
/* code */
}
function baz(this, ...args) {
/* code */
}
let x = (this: number) => {
/* code */
};
A couple additional points about this
parameters to be aware of:
- As you can see in the above example, arrow functions cannot have
this
parameters, as they bindthis
at the definition site, rather than the call site. This would make athis
parameter useless on such a definition. - Functions and methods now check their
this
argument against their annotated (or inferred)this
parameter at call time. So, using the example above,foo.bind({x: 3})()
would error on an incompatibility between the value of3
and its expectedstring
type. - Classes will default to inferring the instance type for the
this
parameter of instance methods, and the class type for thethis
parameter of static methods, unless otherwise annotated. Declared classes and interfaces will default tomixed
. - Class and interface methods will require that their
this
parameter is a supertype of the type of the interface or class, as this is required for subtyping to be sound. Annotating a class method with a specificthis
parameter will affect the type ofthis
inside the method, potentially restricting the properties or methods that can be accessed in the body. - As a result of these changes, using Flow after this change will require updating to a version of Babel that has support for this new syntax: at least version 7.13.0, since in order to resolve incompatibilities between
this
parameters, you may need to explicitly annotate them. Additionally, in future versions of Flow (more on this in another upcoming post), we will be requiring explicitthis
parameter annotations on top-level functions usingthis
in the body.
This has a number of benefits: it allows Flow to make stronger guarantees about the this
parameters of methods, as well as type patterns we previously could not support. For example, in the past we loosely typed Array
methods like map
using mixed
for the callback’s thisArg
, which did not actually check against the callback properly. Now instead of writing the method definition for map
like this:
map<U>(callbackfn: (value: T, index: number, array: Array<T>) => U, thisArg?: mixed): Array<U>
we can instead type map
more safely as:
map<U, This>(callbackfn: (this : This, value: T, index: number, array: Array<T>) => U, thisArg: This): Array<U>
Restrictions on method unbinding
Consider the following code sample in which a class method is rebound, resulting in a runtime error:
class B {
m() {}
}class C extends B {
bar = () => null;
m() {
this.bar();
}
}var x: B = new C();
x.m.apply(new B()); // rebinding leads to a runtime error!
This will error at runtime, as B
does not have a defined bar
property, but x.m
requires bar
to exist on its this
argument. One might wonder what this
parameter we might infer here that would detect this error, but it turns out that there is in fact no practical sound way to prevent this type of runtime error from occurring without banning subclassing outright. In order to ensure that this
parameter annotations are sound, starting with version 0.153.0 Flow will ban the unbinding of class methods from the context in which they were defined.
This means that all of the following will error:
class A {
m() {}
}let a = new A();// error: explicitly unbinding m from a
let b = a.m;// error: bind, apply and call cannot be used with class methods
a.m.bind({}); // error: destructuring implicitly unbinds m from a
let { m } = a;
Flow’s LSP interface supplies quickfixes for these new errors, so if you run into them while writing code, you will be able to apply quickfixes to resolve them automatically. Additionally, in order to facilitate upgrading your codebase past this change, we are introducing a new command to Flow’s CLI interface: fix
. Running flow fix --write
in your project will modify your code by applying the quickfixes for these new errors in all the places they show up. In particular, the fix
command will modify unbound methods to become arrow method properties. In the above example, the class A
would be rewritten as:
class A {
m = () => {}
}
It is worth noting that this may introduce variance errors (since methods are covariant) that may need to be fixed, and is not entirely safe if you were explicitly rebinding these class methods or accessing them directly from the class prototype. In particular, you may need to ensure that class hierarchies have the same type of definition for matching fields. I.e., if m
is a method on A
, it should be a method on all superclasses and subclasses of A
. The same principle holds for function or arrow properties. In cases where methods cannot be easily transformed into arrows, we suggest suppressing the unbinding errors to ease the version upgrade.
Restrictions on this
in objects
Flow will be banning the use of this
inside of object methods. We recommend assigning the object to a variable and referencing that to achieve the same kind of behavior. For example, you could rewrite this code:
let o = {
x: "",
write(s: string) {
this.x = s;
},
read() {
return this.x;
},
};
into:
let o = {
x: "",
write(s: string) {
o.x = s;
},
read() {
return o.x;
},
};
It is difficult to automate this change, so we suggest that in order to upgrade past this change, you suppress the new object-this-reference
errors that appear and fix instances of this as you encounter them in your code.
Changes to class-object subtyping
The final change in this set reorganizes the type hierarchy for objects and classes. Currently in Flow, an inexact object that defines the same fields and methods as a class is a valid supertype of an instance of that class. In version 0.153.0 of Flow, we are changing this behavior: objects will no longer be valid supertypes of class instances; instead, interfaces can be used to achieve the same effect. So code like the following:
function foo(x: { p: number, ... }) {
/* code */
}
class A {
p: number;
}
foo(new A()); // class-object-subtyping error
can be rewritten like this:
function foo(x: interface { p: number }) {
/* code */
}
class A {
p: number;
}
foo(new A());
This restriction was put in place to make the previously mentioned changes safe; since we are adding differing behavior to classes and objects, it no longer makes sense to be able to cast between them. Object types will now be distinct from classes, while interfaces will express the supertype of both structures. Conceptually, you can think of an interface as a type describing the accessible properties and methods on a value, whether it’s a number, a class or an object. Interfaces are similar to inexact objects in their subtyping behavior, so all the following are valid uses of interfaces:
let a: interface { length: number } = "";let b: interface { toUpperCase(): string } = "";let c: interface { toFixed(fractionDigits?: number): string } = 3;let d: interface { x: string } = { x: "", y: "" };let e: interface { x: string } = class {
static x = "";
static y = "";
};let f: interface { x: string } = new (class {
x = "";
y = "";
})();
However, interfaces will also have the same restrictions as classes on unbinding their methods.
In order to facilitate upgrading your codebase past this change, the fix
command mentioned above will include an autofix that automatically refactors object types to interfaces when they appear as supertypes of classes (the example above with the function foo
demonstrates what this change will look like). As was the case previously, this autofix will also be applicable directly from the IDE when you encounter this error.