Search code examples
flowtypestructural-typing

Flow: shape types and additional optional fields


I often find myself needing to do the following, and Flow makes it stupidly difficult:

/* @flow */

type Foo = {
  +foo?: number,
}

type FooBar = {
  +foo?: number,
  +bar?: number,
}

const foo: Foo = {foo: 2}

function process(arg: $ReadOnly<FooBar>) {
}

process(foo)

Is there any good way to do this? I get the following error:

17: process(foo)
            ^ Cannot call `process` with `foo` bound to `arg` because property `bar` is missing in `Foo` [1] but exists in `FooBar` [2].
References:
12: const foo: Foo = {foo: 2}
               ^ [1]
14: function process(arg: $ReadOnly<FooBar>) {
                                    ^ [2]

Solution

  • Flow defaults to object types being inexact; this means that, although Foo is only declared to explicitly have a single (optional) property foo of type number, a variable of type Foo could theoretically have additional properties. For example, a variable of type Foo could have a property bar. And, since bar is not typed in Foo, bar's type is unrestricted (i.e., not necessarily number). As a result, if you were to interact with arg.bar in process and Flow allowed arg to be of type Foo, you are not guaranteed to be interacting with a number. Thus, Flow complains.

    One way to fix this is to use exact object types. If Flow knows that a variable of type Foo will never have a bar property, then it can safely be passed into process. So, you could type Foo as:

    type Foo = {|
      +foo?: number,
    |};
    

    Try Flow

    (Note: the $ReadOnly utility type is not necessary with the above definition of Foo and the original definition of FooBar because bar is not writable. Though, certainly, it's fine to keep $ReadOnly.)