Flow tips and tricks, part 4: Type vs Interface

NGUYEN Trung
3 min readJun 10, 2018

--

Problem

Lots of JS developers who actually use Flow have some TypeScript backgrounds and I believe that some of them got confused with the keywords interface and type at the beginning. Both these keywords are valid in both worlds Flow and Typescript.

Let’s say we have:

/* @flow */
type SerializableObj = {
serialize(): string
};
interface Serializable {
serialize(): string;
};

These declarations seem to be the same thing. In simple cases one can be used interchangeably with the other. So how do we choose type or interface?

In the scope this post I try to explain some little differences between interface and type in Flow.

Explanation

From the documentation, if we change the official example:

interface Serializable {
serialize(): string;
}
class Foo implements Serializable {
serialize() { return '[Foo]'; }
}

to:

type Serializable = {
serialize(): string;
}
class Foo implements Serializable {
serialize() { return '[Foo]'; }
}

we get a pretty clear error:

So the major difference is if we want to use implements for ES2015 Class, we must use an interface, not a type. Simple is that!

Again from the docs:

Classes in Flow are nominally typed. This means that when you have two separate classes you cannot use one in place of the other even when they have the same exact properties and methods.
...
you can use interface in order to declare the structure of the class that you are expecting.

Using implements for ES2015 isn’t the only way to guarantee the structure of a class instance. We can give to the last one with a compatible type that has the expected structure:

type Serializable = {
serialize(): string;
}

class Foo {
serialize() { return '[Foo]'; }
}

const foo: Serializable = new Foo();

The last note I would like to call our attention is about the difference between two following declarations:

interface Serializable_Method {
serialize(): string;
}

interface Serializable_Attribute {
serialize: () => string;
}

By default, any methods declared on an interface in Flow is read-only because all these methods will be implemented in classes that implements this interface. This is the case for the first declaration.

The second declaration indicates that serialize is a property of Serializable_Attribute. Again from the documentation:

Interface properties are invariant by default.

So if we have

class Foo implements Serializable_Method {
serialize() {
return '[Foo]';
}
}

const foo: Serializable_Attribute = new Foo();

We will get a pretty clear error message from Flow:

In practice

I suppose that Flow team is trying to unify these two concepts type and interface (as showed in this discussion link). With that link, I extract the best pratices for using type and interface:

- 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.

In practice, I usually use interface inside the library definition to define third-party libraries on my own, type for the rest of the JS codebase.

--

--