Flow tips and tricks, part 4: Type vs Interface
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.