Advanced features in Flow
Flow has some very interesting features that are currently not documented. It is likely that the reason for missing documentation is that these features are still experimental. Caveat emptor.
I took a stroll through the source code for Flow v0.11. Here is what I found while reading type_inference_js.ml and react.js.
Edit: It has been pointed out to me that Flow features prefixed with $
are not technically public,
and that the semantics of those features may change.
But they are useful enough that I plan to use some of them anyway :)
Table of Contents
Class<T>
Type of the class whose instances are of type T
.
This lets you pass around classes as first-class values -
with proper type checking.
I use this in an event dispatch system where events are class instances, and event handlers are invoked based on whether they accept a given event type:
This gives me a compile-time guarantee that event handlers can handle events of the type they are invoked with.
// No errors!
ViewPost,
// Error: object pattern property not found in ViewComment
ViewComment,
$Diff<A,B>
If A
and B
are object types,
$Diff<A,B>
is the type of objects that have properties defined in A
, but not in B
.
Properties that are defined in both A
and B
are allowed too.
React uses this to throw type errors if components are not given required props, but to leave props with default values as optional.
;
Where P
is the type for component props,
and D
is the type for default props.
Here is a simplified example:
// No errors!
// No errors!
// Error, because `bar` is required
In my testing,
this worked equally well if $Diff
was used directly in the type of P
instead of as a type bound.
But in the React type declarations,
it is used in a type bound.
$Shape<T>
Matches the shape of T
.
React uses $Shape
in the signatures for setProps
and setState
.
Where P
is the type of a component’s props,
and S
is the type of a component’s state.
An object of type $Shape<T>
does not have to have all of the properties that
type T
defines.
But the types of the properties that it does have must match the types of the
same properties in T
.
In React this means that you can use, e.g., setState
to set some state
properties, while leaving others unspecified.
Note how the type of state
in replaceState
differs:
when calling replaceState
you must include a value for every property in S
.
Some examples:
// Error: string is incompatible with type number
An object of type $Shape<T>
is not allowed to have properties that are not
part of T
.
// Error: prop baz of Props not found in object type
$Shape
is like a supertype for objects.
Consider that a type { foo: number }
(when used as a type bound) represents
all objects that have a foo
property of type number
.
That includes objects that have additional properties, such as
{ foo: 1, bar: 'string' }
.
So { foo: number }
is a supertype of, e.g., { foo: number, bar: string }
.
The type $Shape<{ foo: number, bar: string }>
allows values of type
{ foo: number }
.
That is to say, $Shape<T>
refers to types that are more general than T
-
i.e., supertypes.
$Record<T>
Update: $Record<T>
is gone in Flow v0.12.
But instead of $Record<T>
,
you can use the nearly equivalent construct: {[key: $Enum<T>]: U}
,
where U
is the type of values in the record.
The type of objects whose keys are those of T
.
This means that an object of type $Record<T>
must have properties with all of
the same names as the properties in T
-
but the types assigned to those properties may be different.
React uses $Record
to check the type of propTypes
.
Here is a simplified example:
// Error: string literal bar property not found in object literal
Note that Flow does not verify that React.PropTypes.number
matches the type
number
.
It just checks that propTypes1
has all of the same keys.
// No errors!
A $Record
type might have additional keys that are not included in the
original object type.
// No errors!
An important detail to note is that a $Record
type can only use property
names that are known statically.
Dynamic lookups are not allowed.
propTypes4.foo // No errors!
// Error: computed property/element cannot be accessed on record type
propTypes4
In its real definition,
React actually uses $Record
in combination with $Supertype
so that you do
not get an error if you omit some properties from propTypes
.
$Supertype<T>
A type that is a supertype of T
.
React uses $Supertype
in the type of propTypes
:
Where the type variable P
is the type of the props for a component.
It looks like the intent is to check that if a component defines propTypes
,
all of the properties listed are also in the type of props
.
But it should not report an error if propTypes
excludes some props.
I could not get a type error when testing this.
It could be that interaction between $Supertype
and object types is still
a work in progress.
If you are thinking of using $Supertype
with an object type,
consider $Shape
-
it might be a better choice.
$Subtype<T>
A type that is a subtype of T
.
This is what you get when you use a type bound.
For example,
these signatures are equivalent:
But the second version has the disadvantage that you cannot refer to the type
that obj
gets in the return type of the function, or in the types of other
arguments.
While trying to come up with example cases for $Subtype
,
I came across other nice improvements to Flow that render $Subtype
unnecessary in a lot of cases.
In a previous version of Flow,
I recall (possibly incorrectly) having to use a type bound in a function that
takes on object with
certain required properties,
where you don’t want to prevent the caller from including additional properties.
But now this works without a type bound:
// No errors!
I also had a thought that $Subtype
could be used to implement a composition
pattern that previously did not work.
But in Flow v0.11 this does work. Hooray!
$Subtype
could be useful if you want to define an object type that can be
assigned properties that are not declared in the type:
a.baz = 3 // No errors!
// Error: property foo not found in object literal
However this weakens property checking on the extensible type.
For example,
Flow does not infer that the type of a.foo
must be number
.
(It checks the type of foo
correctly when the object is first created,
but not on reads or reassignment).
$Enum<T>
The set of keys of T
.
One use for this is to write a lookup function,
and have Flow check that the lookup key is valid.
'foo' // No errors!
// Error: string literal nao property not found in object literal
'nao'
The type $Enum<typeof props>
is a lot like this type:
But with $Enum
,
Flow computes the type union for you.
Flow’s tests include more examples.
Existential types
Flow supports an “existential type”: *
.
When *
is given as a type,
it acts as a placeholder,
leaving it to the type checker to infer the type for that position.
Let’s generalize the getProp
function from the section above.
Let’s write a function that takes any object and returns a getter.
The getter can be used to get arbitrary properties out of the object,
without having to refer to the object itself after creating the getter.
We would like to write this:
// Does not work!
The use of $Enum<T>
ensures that a getter can only be called to get
properties that actually exist on the given object.
But Flow objects to the lack of type declarations in the inner function. It reports an error:
function type is incompatible with polymorphic type: function type
An obvious idea is to copy types from the return type of makeGetter
into the
signature of the inner function.
But that leads to another problem.
// Still does not work!
This time the error is:
identifier T, Could not resolve name
The problem is that type variables in a function signature
(in this case, in the signature of makeGetter
)
are not in scope in the function body.
So we cannot refer to the type T
in inner function types,
or in variable types in the body of makeGetter
.
On the other hand, Flow might be smart enough to figure out what the argument
type in the inner function should be.
So we can use *
to kick the problem over to Flow.
// Finally, an version that does work.
Now we can see makeGetter
in action.
'foo' // No errors!
'baz' // string literal 'baz' not found in object literal
Another option would have been to use any
instead of *
.
But that is so much less elegant!
The important difference is that where *
appears,
Flow will fill in a specific type,
which could lead to accurate type checking in other areas of the code where the
value of type *
appears.
If you annotate a value with any
,
Flow will not attempt to type-check expressions where that value appears -
which could lead to type errors being missed.
In this case the choice of *
versus any
does not matter,
since the outer function has the type signature that we want.
React uses an existential type to define the ReactClass
type:
/**
* Type of a React class (not to be confused with the type of instances of a
* React class, which is the React class itself). A React class is any subclass
* of ReactComponent. We make the type of a React class polymorphic over the
* same type parameters (D, P, S) as ReactComponent. The required constraints
* are set up using a "helper" type alias, that takes an additional type
* parameter C representing the React class, which is then abstracted with an
* existential type (*). The * can be thought of as an "auto" instruction to the
* typechecker, telling it to fill in the type from context.
*/
;
;
This allows React to pass around a polymorphic class as a first-class value without losing type information.
Scoped type variables in classes
I mentioned in the last section that type variables in a function’s signature are not in scope in the body of that function. I find it interesting that classes do not have this restriction: type variables in a class declaration are in scope in the class definition. (They are not in scope within method bodies; but you can use these variables in method signatures and class variable types). Because of this, there are some problems that do not exactly work with functions but that do work with classes.
We can reimplement makeGetter
from the last section as a class.
'foo' // No errors!
// Error: string literal 'baz' not found in object literal
'baz'
This probably seems unremarkable -
every object-oriented language with static type-checking scopes class-level
type variables this way.
But ES6 classes are mostly syntactic sugar for ES5 constructor functions -
yet a straight translation of Getter
to ES5 syntax does not work:
// Does not work
We get the same error: identifier T, could not resolve name
.
This time to fix the problem we have to refer to T
indirectly using typeof obj
.
// No errors!
The ability of classes to scope type variables over inner methods,
and the ability to use instanceof
for type refinement,
lead me to use classes more often than I would if I were not using Flow.
Revisions
2015-06-01 —
Added notice that $
features are not public.
2015-06-08 —
$Record<T>
is gone in Flow v0.12.