Flow’s best kept secret

One of the features I most often turn to when I’m frustrated by flow’s errors is covariance. That sounds scary (I think this is why it’s not more widely known about) but covariance is actually just a fancy word for read-only. I’ll use the word read-only from now on, so you can forget all about that silly word.

Although it’s much less commonly used (in my experience), contravariance can also be useful. Contravariance is just a fancy word for write-only, so from now on I’ll just call it write-only.

Read Only Properties

Lets consider an example of where you need to mark something as read-only in order for your JavaScript code to type check properly. The following code works fine, even if it is a little contrived:

// @flow
function logStringContainer(container: {value: string}) {
logNullableStringContainer(container);
}
function logNullableStringContainer(container: {value: ?string}) {
if (container.value != null) {
console.log(container.value);
}
}
logStringContainer({value: 'foo'});

Despite it working fine, flow will give us the following error:

example.js:4
4:   logNullableStringContainer(container);
^^^^^^^^^
object type. This type is incompatible with the expected param
type of
7: function logNullableStringContainer(container: {value: ?string})
^^^^^^^^^^^^^^^^
object type
Property `value` is incompatible:
  {value: ?string}
^^^^^^^ null. This type is incompatible with
{value: string}
^^^^^^ string

This is interesting; intuitively, it feels like it should be fine to pass a string into a function that is expecting a ?string, but the problem turns out to be that we could decide to write to that property, as well as reading from it. In fact, inside logNullableStringContainer, flow would see it as completely acceptable for you to set container.value to null. This would violate the contract that of logStringContainer. It would be possible for someone to pass in an object, and be surprised when a property was set to null.

Because we are only reading from the property, there is no bug. All we need to do is tell flow that the property is read-only (covariant). To mark a property as read only, you just prefix it with a +.

// @flow
function logStringContainer(container: {+value: string}) {
logNullableStringContainer(container);
}
function logNullableStringContainer(container: {+value: ?string}) {
if (container.value != null) {
console.log(container.value);
}
}
logStringContainer({value: 'foo'});

Now when we run flow, we get a result of:

No errors!

Read Only Arrays

The above example only works for when you’ve typed things as plain objects. It won’t work for generics (Arrays, Sets, Maps etc.). Lets take an example of some more code that takes an array, and works fine — but doesn’t type check.

Edit: for Arrays, there is now a built in $ReadOnlyArray<T> that you can use, to handle the Array case, however the following technique is still useful for other containers that you want to treat as read only.
Thanks to @GiulioCanti for pointing this out
// @flow
function logStrings(values: Array<string>) {
logNullableStrings(values);
}
function logNullableStrings(values: Array<?string>) {
values.forEach(value => {
if (value != null) {
console.log(value);
}
});
}
logStrings(['a', 'b', 'c']);

This clearly works fine, but flow will report:

example.js:4
4:   logNullableStrings(values);
^^^^^^^^^^^^^^^^^^^^^^^^^^ function call
7: function logNullableStrings(values: Array<?string>) {
^^^^^^^ null.
This type is incompatible with
3: function logStrings(values: Array<string>) {
^^^^^^ string

This time, we can guess at what’s gone wrong. Flow can’t tell that we aren’t going to push a new value into the array of values and if we did, we would be within our rights to push null into the array passed to logNullableStrings, but not the array passed to logStrings.

Unfortunately, there’s no easy way to indicate that the array is read only, but we can tell flow what types of array we think should be accepted, and it will prevent us from modifying the array. To do this, we make some clever use of generics:

// @flow
function logStrings<T: string>(values: Array<T>) {
logNullableStrings(values);
}
function logNullableStrings<T: ?string>(values: Array<T>) {
values.forEach(value => {
if (value != null) {
console.log(value);
}
});
}
logStrings(['a', 'b', 'c']);

Once again, flow reports:

No errors!

This works, because now we are saying we accept Array<T> where T is a ?string. The <T: ?string> is the bit that constrains T to being some sub-class of ?string. For each call of logNullableStrings, that T could be substituted with something else. This means that the argument could end up being Array<string> or Array<null> or Array<?string> or Array<'foo' | 'bar' | void> or any number of other substitutions. Flow is clever enough to pick the right one for the occasion.

Note how this can also be useful if you want to pass through nulls in a function. e.g.

// @flow
function getValueIfNotNull<T: ?string>(container: {value: T}): T {
return container.value;
}
// flow can tell that the result of the function is not-null if
// the input is not-null
const value: string = getValueIfNotNull({value: 'foo'});

Write Only Properties

Although I’ve found it is generally far less useful, it is possible to mark properties as write-only, using  instead of a +. This lets you type code like the following:

// @flow
function setNullableStringContainer(container: {-value: ?string}) {
setStringContainer(container);
}
function setStringContainer(container: {-value: string}) {
container.value = 'foo';
}
const container = {value: null};
setNullableStringContainer(container);

Write Only Arrays

The approach used for read only arrays (called bounded polymorphism) doesn’t work for write only arrays. The way to handle write only arrays is by declaring an interface, and marking that interface as being write-only (contravariant) for the generic parameter. For example, we could declare the following interface:

// @flow
declare interface WriteableArray<-T> {
push(...items: Array<T>): number;
unshift(...items: Array<T>): number;
}
function pushNullableString(values: WriteableArray<?string>) {
pushString(values);
}
function pushString(values: WriteableArray<string>) {
pushFoo(values);
}
function pushFoo(values: WriteableArray<'foo'>) {
values.push('foo');
}
pushNullableString([]);

Because Array does have both those two methods, flow will let us pass an Array to a method that expects a WriteableArray. Because all the methods implemented are write-only, we can mark the type parameter T as write-only using <-T>.