Classes
Classes were introduced in JavaScript to formalize the common practice of simulating class-like inheritance hierarchies in JavaScript with functions and prototypes.
In Flow, a class may be defined using standard syntax extended with field declarations.
class C {
x: string;
y: number;
constructor(x) { this.x = x; }
foo() { return this.x; }
bar(y) { this.y = y; }
}
class D extends C {
foo() { return super.foo() + "!"; }
bar(y) { super.bar(y || 0); }
static qux() { return new D("hello"); }
}
In the code above, class C
defines fields x
and y
, which are typed as
string
and number
, respectively. C
also defines a constructor and a
few methods. class D
then overrides those methods, and adds a static method.
Just like other languages with classes, Flow enforces that the type of
an overridden method in a superclass (e.g., bar
in C
) is compatible with
the type of an overriding method (e.g., bar
in D
). This ensures that
subclassing implies subtyping, i.e., the following code type checks:
var c: C = new D("D extends C");
Type Annotations vs. Inference#
Inference, Locally#
Notice that the class definitions above have no type annotations, apart from their field declarations. Flow uses type inference extensively in local contexts, to avoid excessive annotation (and annotating).
In the above classes, all methods are in fact strongly typed: Flow has propagated types through method calls and field accesses to infer a static type for each method. So, for example, the following code fails to typecheck:
1 |
var n: number = c.foo(); // string is incompatible with number |
$> flow
1: var n: number = c.foo(); // string is incompatible with number
^^^^^^^ string. This type is incompatible with
1: var n: number = c.foo(); // string is incompatible with number
^^^^^^ number
Annotations, Globally#
In general, type inference adds convenience and helps to keep code from becoming cluttered with redundant type annotations. But a lack of annotations can also reduce clarity - in particular, when errors occur far from their causes.
For this reason, Flow requires exported definitions be more thoroughly annotated than ones used only locally.
Exported classes must annotate their method signatures - parameter and return types - in addition to field declarations:
class ExportC {
x: string;
y: number;
constructor(x: string) { this.x = x; }
foo(): string { return this.x; }
bar(y: number): void { this.y = y; }
}
class ExportD extends ExportC {
foo(): string { return super.foo() + "!"; }
bar(y?: number): void { super.bar(y || 0); }
static qux(): ExportD { return new ExportD("hello"); }
}
module.exports.C = ExportC;
module.exports.D = ExportD;
Structural vs. Nominal typing for Classes#
Flow treats classes as nominal types: structurally identical classes
are not interchangeable, and one class is only a subtype of another
if it has been explicitly declared as a subclass using extends
.
1 2 3 4 5 6 7 8 9 10 |
// structurally identical to C, but nominally unrelated class E { x: string; y: number; constructor(x) { this.x = x; } foo() { return this.x; } bar(y) { this.y = y; } } var eAsC: C = new E("hi"); // nope, E is incompatible with C |
$> flow
10: var eAsC: C = new E("hi"); // nope, E is incompatible with C
^^^^^^^^^^^ E. This type is incompatible with
10: var eAsC: C = new E("hi"); // nope, E is incompatible with C
^ C
However, Object and Interface types are structural. Classes implement interfaces, and satisfy object shapes, on a structural basis:
// class C has everything it needs to satisfy this interface
interface ILikeC {
x: string;
y: number;
foo(): string;
bar(y: number): void;
}
function takesAnILikeC(c: ILikeC): string { return c.foo(); }
var c: C = new C("implements ILikeC");
var s: string = takesAnILikeC(c);
// similarly, C satisfies this object shape
type XY = { x: string; y: number; };
function takesAnXY(xy: XY): number { return xy.y; }
var c: C = new C("satisfies XY");
var n: number = takesAnXY(c);
Polymorphic classes#
Class definitions can be polymorphic, meaning that they can represent a family of classes of the same “shape” but differing only in the instantiation of their type parameters.
Consider a polymorphic version of class C
above:
class PolyC<X> {
x: X;
y: number;
constructor(x: X) { this.x = x; }
foo() { return this.x; }
bar(y) { this.y = y; }
}
The class C
is polymorphic in the type parameter X
. Flow checks that the
parts of C
which refer to X
are correct for any instantiation of X
.
Thus, when class InstanceD
extends PolyC<string>
,
Flow can conclude that the latter has a method with signature
foo(): string
, and (as usual) check that it is compatible
with the type of foo
in InstanceD
.
1 2 3 4 |
class InstanceD extends PolyC<string> { foo() { return super.foo() + "!"; } bar(y) { super.bar(y || 0); } } |
$> flow
2: foo() { return super.foo() + "!"; }
^^^^^^^^^^^ X. This type cannot be used in an addition because it is unknown whether it behaves like a string or a number.
Bounded polymorphism#
Type parameters can optionally specify constraints that are enforced on instantiation. Such constraints can be assumed to hold in the body of a polymorphic class definition. See this blog post on bounded polymorphism for details.
Polymorphism and Type Parameter Variance#
By default, polymorphic classes are invariant in their type parameters,
which means that an expression of type C<T>
may flow to a location typed
C<U>
only when T
and U
are simultaneously subtypes of each other.
For example, this read/write map of values of type V
is only compatible
with another map of values whose type is both a subtype and supertype
of V
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class A { x: number; } class B extends A { y: string; } class ReadWriteMap<K, V> { store: { [k:K]: V }; constructor() { this.store = {}; } get(k: K): ?V { return this.store[k]; } put(k: K, v: V): void { this.store[k] = v; } } declare var mapOfB: ReadWriteMap<string, B>; // error: mapOfB.get(k): A is fine, but consider mapOfB.put(k, (a: A)) var mapOfA: ReadWriteMap<string, A> = mapOfB; |
$> flow
14: var mapOfA: ReadWriteMap<string, A> = mapOfB;
^ A. This type is incompatible with
11: declare var mapOfB: ReadWriteMap<string, B>;
^ B
However, a polymorphic class may specify that a given type parameter is co- or contravariant, meaning that one instance is compatible with another if one type argument is a sub- or supertype, respectively, of the other.
Covariance is useful when a type parameter only appears in output (or “positive”) positions within a class definition:
class ReadOnlyMap<K, +V> {
store: { +[k:K]: V };
constructor(store) { this.store = store; }
get(k: K): ?V { return this.store[k]; }
}
declare var readOnlyMapOfB: ReadOnlyMap<string, B>;
// ok: B is a subtype of A, and V is a covariant type param.
var readOnlyMapOfA: ReadOnlyMap<string, A> = readOnlyMapOfB;
Analogously, contravariance is useful when a type parameter only appears in input (or “negative”) positions within a class definition.
Note: type parameter variance may be specified in any polymorphic types, not just polymorphic classes.
This type#
Within a class definition, the this
type is available for use
in annotations. this
can improve the precision of certain types
in the presence of inheritance.
Intuitively, the meaning of the this
type is as follows: consider a class
C
and a class D
extending C
. At runtime, the value of this
within
methods of C
will sometimes be an instance of C
, and sometimes an instance
of D
.
Within the body of C
, then, the this
type denotes both C
and D
.
More generally, the this
type behaves exactly like a type parameter on
a class, ranging over that class and all of its subclasses.
An immediate consequence is that this
may only appear in output
(aka covariant, “positive”) positions, if subtyping is to be preserved.
The payoff comes in the form of improved precision in the types of superclass
methods from the perspective of subclasses: for example, a method of C
that returns a value of type this
can safely be viewed as returning D
when
invoked on an instance of D
.
1 2 3 4 5 6 7 8 9 |
class ThisA { x: this; // error: input/output position foo(): this { return this; } // ok: output position bar(x: this): void { this.x = x; } // error: input position } class ThisB extends ThisA { } var b: ThisB = (new ThisB).foo(); // ok: foo() on a ThisB returns a ThisB |
$> flow
2: x: this; // error: input/output position
^^^^ `this` type. invariant position (expected `this` to occur only covariantly)
4: bar(x: this): void { this.x = x; } // error: input position
^^^^ `this` type. contravariant position (expected `this` to occur only covariantly)
Class<T>
type#
The name of a class, used as a type annotation, represents instances of that class, not the class itself. It is often useful, however, to refer to the types of classes.
Given a type T
representing instances of a class C
, the type Class<T>
is
the type of the class C
.
var theClass: Class<C> = C;
var anInstance = new C("foo");
You can edit this page on GitHub and send us a pull request!