When do you use an interface over a type alias in flow?

This is a great question. Ideally there would be no difference between an interface and an object type. As implemented, there are a handful of (often subtle) differences between them.

The biggest difference is that Flow considers methods declared on an interface to be "read-only." This allows subtypes to be covariant w.r.t. methods, which is a very common pattern with inheritance hierarchies.

In time, I would like to see Flow unify these concepts, but until then here's my rule of thumb for choosing between interfaces and object types:

  • Use object types to describe bags of mostly data that are passed around in your app, e.g., props/state for React components, Flux/Redux actions, JSON-like stuff.
  • Use interfaces to describe service-like interfaces. Usually these are mostly methods, e.g., Rx.Observable/Observer, Flux/Redux stores, abstract interfaces. If a class instance is likely to be an inhabitant of your type, you probably want an interface.

Hope this helps!


Interfaces in Flow can be used to make sure a class implements certain methods and properties. For instance:

interface IFly {
   fly(): string
}

// Good!
class Duck implements IFly {
    fly() {
        return "I believe I can fly"
    }
}

// Bad! Cannot implement `IFly` with `Duck` because number is incompatible with string in the return value of property `fly`.
class Duck implements IFly {
    fly() {
        return 42
    }
}

// Bad! Cannot implement `IFly` with `Duck` because property `fly` is missing in `Duck` but exists in `IFly`.
class Duck implements IFly {
    quack() {
        return "quack quack"
    }
}

However, if we define an equivalent IFly type our Duck class won't be able to implement it:

type IFly = {
  fly(): string
}

// Cannot implement `Fly` because it is not an interface.
class Duck implements Fly {
    fly() {
       return "I believe I can fly"
    }
}

In addition, there are more subtle differences between types and interfaces.

By default, interface properties are invariant. For example:

interface Foo {
    property: string | number
}

let foo: Foo = { property: 42 } // Cannot assign object literal to `foo` because number is incompatible with string in property `property`.

To make interface property covariant they need to be made read-only:

interface Foo {
    +property: string | number
}

let foo: Foo = { property: 42 } // Good!

With types, on the other hand, Flow will not complain:

type Foo = {
    property: string | number
}

// Good!
let foo: Foo = { property: 42 }

References:

  1. Flow interfaces
  2. Type variance