Objects
Objects can be created with object literals. The types of properties are fixed based on their initializers.
1 2 3 4 5 6 |
// @flow var o = { x: 42, foo(x) { this.x = x; } }; o.foo('hello'); |
$> flow
6: o.foo('hello');
^^^^^^^^^^^^^^ call of method `foo`
4: foo(x) { this.x = x; }
^ string. This type is incompatible with
3: x: 42,
^^ number
Flow infers the type of property x
of the object to be number since it is
initialized with a number
. The method call foo()
on the object writes
string
to that property. As expected, running Flow produces an error.
Object Types#
Object types are of the form:
{ x1: T1; x2: T2; x3: T3;}
Here is an example of declaring an object type:
// @flow
class Foo {}
var obj: {a: boolean; b: string; c: Foo} = {a: true, b: "Hi", c: new Foo()}
Here is an example of Flow catching a problem with your object type:
1 2 3 |
// @flow class Bar {} var badObj: {a: boolean; b: string; c: Foo} = {a: true, b: "Hi", c: new Bar()} |
$> flow
3: var badObj: {a: boolean; b: string; c: Foo} = {a: true, b: "Hi", c: new Bar()}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ object literal. This type is incompatible with
3: var badObj: {a: boolean; b: string; c: Foo} = {a: true, b: "Hi", c: new Bar()}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ object type
Reusable Object Types#
Object types can be made reusable through the use of type aliases:
// @flow
type MyType = {message: string; isAwesome: boolean};
function sayHello(data: MyType) {
console.log(data.message);
}
var mySampleData: MyType = {message: 'Hello World', isAwesome: true};
sayHello(mySampleData);
sayHello({message: 'Hi', isAwesome: false});
Object types can be added together with the intersection operator, &
. See
union and intersection types
for details.
Optional properties#
Object types can have optional properties. The following code shows how optional properties allow objects with missing properties to be typed.
// @flow
var optObj: { a: string; b?: number } = { a: "hello" };
When optional properties are accessed, Flow tracks the fact that they could
be undefined
, and reports errors when they are used as is.
1 |
optObj.b * 10 // error: undefined is incompatible with number |
$> flow
1: optObj.b * 10 // error: undefined is incompatible with number
^^^^^^^^ undefined. The operand of an arithmetic operation must be a number.
One way to avoid errors is to dynamically check that an optional property exists before using it. See nullable types for details.
Property Variance#
By default, objects are invariant with respect to their property types.
1 2 3 4 5 6 7 |
function invariance(o: {p: ?number}) { let p = o.p; o.p = null; return p; } var subtype_p: {p: number} = {p: 0}; invariance(subtype_p); |
$> flow
7: invariance(subtype_p);
^^^^^^^^^ object type. This type is incompatible with the expected param type of
1: function invariance(o: {p: ?number}) {
^^^^^^^^^^^^ object type
7: invariance(subtype_p);
^^^^^^^^^ object type. This type is incompatible with the expected param type of
1: function invariance(o: {p: ?number}) {
^^^^^^^^^^^^ object type
However, properties can be annotated as covariant:
function covariance(o: {+p: ?number}) {}
covariance(subtype_p);
Covariant properties can not be written:
1 2 3 |
function covariance_err(o: {+p: ?number}) { o.p = null; } |
$> flow
2: o.p = null;
^ object type. Covariant property `p` incompatible with contravariant use in
2: o.p = null;
^^^ assignment of property `p`
Or contravariant:
function contravariance(o: {-p: number}) {
o.p = 0;
}
var supertype_p: {p: ?number} = {p: null};
contravariance(supertype_p);
Contravariant properties can not be read from:
1 2 3 |
function contravariance_err(o: {-p: number}) { return o.p; } |
$> flow
2: return o.p;
^ object type. Contravariant property `p` incompatible with covariant use in
2: return o.p;
^^^ property `p`
Constructor Functions and Prototype Objects#
Another way of creating objects in JavaScript is by using new
on
constructor functions. A constructor function is typically an open method
that “initializes” some properties of this
; and a new
operation on such a
function calls it on a freshly created object before returning it.
Additionally, a constructor function may set various properties on its
prototype
object. These properties are typically methods, and are inherited
by all objects created from that constructor function by a process known as
prototype chaining.
1 2 3 4 5 6 |
// @flow function FuncBasedClass(x) { this.x = x; } FuncBasedClass.prototype.f = function() { return this.x; } var y = new FuncBasedClass(42); var z: number = y.f(); |
In this code, a new
object is created by new FuncBasedClass(42)
; this object
has a property x
initialized by FuncBasedClass
with the number
passed to it.
The object also responds to the f
method defined in FuncBasedClass.prototype
,
so y.f()
reads y.x
and returns it. This fits with the expectation of a number
as
expressed by the annotation at line 6, so this code typechecks.
Furthermore, Flow ensures that an object’s type can always be viewed as a
subtype of its constructor’s prototype type
. (This is analogous to subtyping
based on class inheritance.) This means that the following code typechecks:
var anObj: FuncBasedClass = new FuncBasedClass(42);
Adding properties#
It is a common idiom in JavaScript to add properties to objects after they are
created. In fact, we have already seen this idiom on several occasions above:
when initializing properties of this
properties in a constructor function;
when building a constructor function’s prototype
object; when building a
module.exports
object; and so on.
Flow supports this idiom. As far as we know, this is a type system novelty: supporting this idiom while balancing other constraints of the type system, such as sound subtyping over objects and prototypes, can be quite tricky!
However, for a property that may be added to an object after its creation, Flow cannot guarantee the existence of that property at a particular property access operation; it can only check that its writes and reads are type- consistent. Providing such guarantees for dynamic objects would significantly complicate the analysis; this is a well-known fact (in technical terms, Flow’s analysis is heap-insensitive for strong updates).
For example, the following code typechecks:
// @flow
function foo(p) { p.x = 42; }
function bar(q) { return q.f(); }
var o = { };
o.f = function() { return this.x; };
bar(o);
foo(o);
In this code, when bar(o)
is called, o.x
is undefined; only later is it
initialized by foo(o)
, but it is hard to track this fact statically.
Fortunately, though, the following code does not typecheck:
1 |
var test: string = bar(o); |
$> flow
1: var test: string = bar(o);
^^^^^^ number. This type is incompatible with
1: var test: string = bar(o);
^^^^^^ string
In other words, Flow knows enough to infer that whenever the x
property of
o
does exist, it is a number, so a string
should not be expected.
Sealed object types#
Unfortunately, supporting dynamically added properties means that Flow can miss errors where the programmer accesses a non-existent property by mistake. Thus, Flow also supports sealed object types, where accesses of non-existent properties are reported as errors.
When object types appear as annotations, they are considered sealed. Also, non-empty object literals are considered to have sealed object types. In fact, the only cases where an object type is not sealed are when it describes an empty object literal (to be extended by adding properties to it), an object literal with spread properties, or when it describes a map (see below).
Overall, the weaker guarantee for dynamically added properties is a small cost to pay for the huge increase in flexibility it affords. Specifically, it allows Flow to usefully type check lots of idiomatic JavaScript code, while trusting the programmer to follow the discipline of fully initializing an object before making it available, which effectively ensures that any dynamically added properties during initialization are only accessed after initialization is complete.
In any case, for most objects you can altogether avoid adding properties dynamically, in which case you get stronger guarantees. Furthermore, as described above, object type annotations are sealed, so you can always force sealing by going through an annotation (and sealing is enforced at module boundaries).
Objects as Maps#
An object can be viewed as a map from string
to some value type by setting and
getting its properties via bracket notation (i.e. dynamic accessors), instead of
dot notation. Flow infers a precise value type for the map: in other words, if
you only write number
values to a map, you will read number
values back
(rather than, say, any
).
Such a map can be given a type of the form
type MapOfNumbers = { [key: string]: number };
var numbers: MapOfNumbers = {
ten: 10,
twenty: 20,
};
where string
is the key type and number
is the value type of the map.
Maps as Records#
Viewing an object as a map does not preclude viewing it as a record. However, for such an object, the value type of the map does not interfere with the types of the properties of the record. This is potentially unsound, but we admit it because a sound design would necessarily lead to severe imprecision in the types of properties.
The Object
type#
This type describes “any object” and you can think of it like an
any
-flavored version of an object type.
In JavaScript, everything is an object. Flow is a bit stricter and does not
consider primitive types to be subtypes of Object
.
1 2 3 4 5 |
(0: Object); ("": Object); (true: Object); (null: Object); (undefined: Object); |
$> flow
1: (0: Object);
^ number. This type is incompatible with
1: (0: Object);
^^^^^^ object type
2: ("": Object);
^^ string. This type is incompatible with
2: ("": Object);
^^^^^^ object type
3: (true: Object);
^^^^ boolean. This type is incompatible with
3: (true: Object);
^^^^^^ object type
4: (null: Object);
^^^^ null. This type is incompatible with
4: (null: Object);
^^^^^^ object type
5: (undefined: Object);
^^^^^^^^^ undefined. This type is incompatible with
5: (undefined: Object);
^^^^^^ object type
Many other types can be treated as objects, however. Naturally objects are
compatible with Object
, but so are functions and classes.
1 2 3 4 |
({foo: "foo"}: Object); (function() {}: Object); (class {}: Object); ([]: Object); // Flow does not treat arrays as objects (likely to change) |
$> flow
4: ([]: Object); // Flow does not treat arrays as objects (likely to change)
^^ empty array literal. This type is incompatible with
4: ([]: Object); // Flow does not treat arrays as objects (likely to change)
^^^^^^ object type
Exact Object Types#
As we saw, the object type { x: string }
ensures that an object contains
at least the property x
of type string
. However, { x: string }
may
have other properties in addition to x
.
Sometimes we want to also make sure that x
is the only property of the
object. For this purpose there are exact object types, which use {|
and |}
instead of {
and }
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
type User = { name: string, age: number }; type StrictUser = {| name: string, age: number |}; // Regular object types allow extra properties ({ name: "Foo", age: 27, foo: false }: User); // Exact object types disallow extra properties ({ name: "Foo", age: 27, foo: false }: StrictUser); // Otherwise, they behave similarly ({ name: "Foo", age: 27 }: User); ({ name: "Foo", age: 27 }: StrictUser); ({ name: "Foo" }: User); ({ name: "Foo" }: StrictUser); // Error: 'age' is missing |
$> flow
7: ({ name: "Foo", age: 27, foo: false }: StrictUser);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ property `foo`. Property not found in
7: ({ name: "Foo", age: 27, foo: false }: StrictUser);
^^^^^^^^^^ object type
12: ({ name: "Foo" }: User);
^^^^ property `age`. Property not found in
12: ({ name: "Foo" }: User);
^^^^^^^^^^^^^^^ object literal
13: ({ name: "Foo" }: StrictUser); // Error: 'age' is missing
^^^^^^^^^^ property `age`. Property not found in
13: ({ name: "Foo" }: StrictUser); // Error: 'age' is missing
^^^^^^^^^^^^^^^ object literal
Exact object types are a very useful tool for helping Flow to refine unions of
object types and notice typos on property names and refinements. Because
{ name: string }
only means “an object with at least a name
property”,
Flow can’t be sure that objects of that type don’t also have other
properties. For this reason, Flow won’t error if it sees an access of a
property called, say, nname
because there’s no guarantee that the object
doesn’t actually have a nname
property on it!
You can edit this page on GitHub and send us a pull request!