Notes on Flow 0.99: Callable Properties, Function Statics, and More

Flow released 0.99 (comparing changes) a few days ago. This release contains changes around callable properties, function statics, a few more utilities coming from the React Component realm, bugfixes, and performance improvements for Flow server.

Once again I'm using those bits and bites to understand more about Flow and JavaScript, and this time it'll be mostly around callable properties and function statics, which I'll share about in this post.

🍾 Function statics

Here is one change that caused new errors in our codebase:

Make function type statics stricter

Before this change, we optimistically typed the statics of a function type as any, under the assumption that a function would not be used as an object. In practice, our optimism here is unfounded.

This diff changes the type of the statics object for function types to an empty, inexact object type with Function.prototype for its proto. This choice is sound with respect to the runtime behaviors while also allowing functions with statics to be valid subtypes via width subtyping.

Time for a review on JavaScript. What are function statics?

For a JavaScript class, a static method is a method for the class, not instances of that class.

For a JavaScript function, statics refer to those weird moments where you may have a variable that can be accessed by saying function name dot that variable name because functions are also objects in JavaScript 🤷🏻‍♀️. Consider this greeting function with a counter:

function greeting(name) {
  if (!greeting.counter) {
    greeting.counter = 0
  }
  greeting.counter++
  return 'hello, ' + name
}

So what this change says is that previously Flow did not put any restrictions on function statics, i.e., they were typed to any. In the example above, this would mean that greeting.counter is a field of any, and is therefore unrestricted.

You may type function statics, for example:

function greeting(name) {
  (greeting.counter: number)
  if (!greeting.counter) {
    greeting.counter = 0;
  }
  greeting.counter++;
  return 'hello, ' + name;
}

Play around in the Try Flow and note that if you change the annotation of counter to string, Flow is catching the error:

4:   (greeting.counter: string)
      ^ Cannot cast `greeting.counter` to string because number [1] is incompatible with string [2].
References:
6:     greeting.counter = 0;
                          ^ [1]
4:   (greeting.counter: string)
                        ^ [2]

🐿 New error in our codebase

The real error that happened in our codebase, however, is a bit different.

We write our Redux actions with data wrapped around in a field conventionally referred to as payload. And we normally annotate payload like this:

type ActionPayload = {
  data: string,
  error: string,
}

Sometimes payload can be a function that returns an object containing the data, so we're happy to see this too:

const fetchData = ({
  type,
  payload,
}: {
  type: 'FETCH_DATA',
  payload: () => ActionPayload,
}) => {
  // function body
}

In a few occasions, though, we mistyped the payload in the function form, where the payload is actually just the object. So our code worked fine and no one noticed that the annotation isn't making sense at all:

const fetchData = ({
  type,
  payload,
}: {
  type: 'FETCH_DATA',
  payload: () => ActionPayload, // <- wrooooong, or not?
}) => {
  return payload.data
}

At 0.99, as function statics are now typed to {}, Flow is complaining that we cannot get data because data is missing in statics of function type.

A similar scenario is a mixture of actions and action creators. They get passed around quite often, which makes typing even hairier. I realize that Flow really is getting in our way when we try to be smart in this direction. I like the flexibility but the complaints will keep asking me whether this is a good pattern or not, which I do not yet have an answer.

👾 Callable property syntax

Remove deprecated $call property syntax

First of all, to not be confused, this is not the $call utility type, which helps you get the return type of a given function, and is not deprecated nor removed.

The deprecated $call property syntax was the precursor of this callable syntax, both the addition of the new syntax and the deprecation of the old happened in 0.75.

So what are they about, anyway?

It ties back to the fact we mentioned earlier that with JavaScript functions are also objects. And here's an interesting insight from Flow:

An object with a callable property can be equivalently viewed as a function with static fields.

And so it turns out:

// You should be able to call objects with call properties
function a(f: { (): string }, g: { (x: number): string }): string {
  return f() + g(123)
}

// ...and get an error if the return type is wrong
function b(f: { (): string }): number {
  return f()
}

And similarly:

// You should be able to use an object as a function
function a(x: { (z: number): string }): (z: number) => string {
  return x
}

Most amazing #TIL for me is perhaps this:

// Multiple call properties should also be supported
function a(f: { (): string, (x: number): string }): string {
  return f() + f(123)
}

// It should be fine when a function satisfies them all
var b: { (): string, (x: number): string } = function(x?: number): string {
  return 'hi'
}

And here is another one that reminds us about functions, that those monadic chained calls made possible by prototypes come down to not much more than a field in a function:

// Expecting properties that do exist should be fine
var b: { apply: Function } = function() {}

So that's a really interesting feature that Flow possesses! That means you can annotate memoized functions with Flow and I never even thought of that before learning all this. Consider a memoized factorial (Try Flow) and you can annotate it expressively with Flow:

type MemoizedFactorialType = {
  cache: {
    [number]: number,
  },
  [[call]](number): number,
}

const factorial: MemoizedFactorialType = n => {
  if (!factorial.cache) {
    factorial.cache = {}
  }
  if (factorial.cache[n] !== undefined) {
    return factorial.cache[n]
  }
  factorial.cache[n] = n === 0 ? 1 : n * factorial(n - 1)
  return factorial.cache[n]
}

There are a few more changes around callable properties in this version, which I'll list out their references:

And here is a couple more links to some older commits you may want to look at regarding callable properties:

🦖 Other notable changes

There are few more notable changes I'll just list here for now:

If you find any of those interesting and have learned something about them, please please write about them because ever since 0.85 my learning about Flow has felt like sitting in a class where no one asks any questions but apparently not many people understand what's going on 🤦🏻‍♀️. Secretly I'm also not completely certain about things I said about Flow. If you spot any mistakes, please do let me know.

💯 Till next time

Flow 0.100 is released too. If you haven't noticed, the release notes now come with Try Flow examples. Maybe the added digit indicates a new era.