A dam with mountains in the background
Photo by ciboulette from Pexels

Flow’s Improved Handling of Generic Types

tl;dr: Flow has improved its handling of generic types by banning unsafe behaviors previously allowed and clarifying error messages.

We’ve improved Flow’s ability to detect errors in generic function and class definitions, and also introduced a new restriction on how generic types can be used. This post describes the new restriction and how to upgrade.

The escaped-generics restriction

In Flow 0.136, we introduced a new check that detects, and raises an error, when Flow infers that an unannotated variable or parameter has a type that includes a generic, but is not inside the scope where that generic is defined. We call this detecting when a generic “escapes” from it’s scope, and it can happen in situations like this [example]:

Since it’s unannotated, Flow will infer the type of external_var, and find it to be T | number, but this isn’t really a type that makes sense for it to have: T doesn’t exist in the scope in which it’s defined! We call this T escaping its scope. Previously this was allowed by Flow, but it had the potential of producing confusing error messages, it wasn’t clear what it means for a variable to have a generic type when the generic is not in scope, and it blocked ongoing work on improving Flow’s generic system as a whole. Other languages likewise object to generic escapes: for example, TypeScript warns that external_var’s type cannot be determined and uses any instead.

The new error we’ve introduced only applies when a generic escapes into an unannotated variable (or property or function return), because if an annotation exists, Flow won’t need to infer anything, and we already ban out-of-scope generics from being explicitly annotated.

The easiest way to fix escaped-generics errors is to simply provide a type annotation, and the error message used by the escape check can usually point to exactly where the annotation is needed. In some cases, as with this example, there may not be a reasonable annotation to use, which likely indicates a more fundamental problem in the program.

this types are also generics

It’s important to note that everything described here also applies to the type this when used inside a class definition. Flow models the this-type as a generic whose upper bound is its enclosing class, and for good reason: a generic can be thought of as a range of types from empty up to its upper bound (which is mixed if not otherwise specified), and since classes can be extended, the this type likewise represents a range of types that include all possible subclasses of the enclosing class.

Detailed example

Previously, Flow would allow escaping to occur without errors, but when the escaped generic is used (as in return external_var above), it would often produce a confusing error:

This is confusing, because how can T be incompatible with itself? On the other hand, Flow needs to produce some error here, because the escaped generic could otherwise be used to break type soundness without the programmer realizing it [example]:

Here, because T escapes its scope and then re-enters it, we’re able to return a value that is actually at runtime a string into a position that expects it to be a boolean—or at least we would be able to if Flow didn’t report an error telling us that something about f was wrong.

The change we’ve made for v0.136 is that now, any generic escape is treated as an error, so in addition to the “T is incompatible with T” error, Flow also produces a more useful error at the site where the generic actually escapes:

We made this change because escaped generic types rarely make any sense — even in cases where they don’t actually cause unsoundness or produce errors, they always have the potential of creating confusing error messages. In addition, we’re making general improvements to Flow’s type-checking of generics, which will remove altogether the confusing error messages above, but which require this additional check to ensure soundness in cases like the above example.

How to upgrade by automatically adding annotations

To make it easier to fix the new escaped-generic errors, in Flow 0.137 we’ve added a new recipe to Flow’s codemod command, which adds annotations to variables that contain escaped generics. This follows the same approach as adding annotations for Types First but should be a smaller change for most codebases.

To add annotations to fix escaped-generics, you can run the following command:

This command update the project at /path/to/folder in-place. This will likely fix many escaped-generics errors, but it does require manual follow up: there may still be some annotations that need to be added manually, in some cases the new annotations will themselves cause errors, and some programs (the program shown above, for example) may need more significant changes because no annotation would allow the program to typecheck as written.

See the documentation for the annotate-exports codemod for more guidance on using the codemod command.

The official publication for the Flow static type checker…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store