Reputation: 75
According to the Subtypes of objects documentation of flow, this works
// @flow
type ObjectA = { foo: string };
type ObjectB = { foo: string, bar: number };
let objectB: ObjectB = { foo: 'test', bar: 42 };
let objectA: ObjectA = objectB; // Works!
But a deeper implementation of this doesn't
// @flow
type ObjectA = { foo: { bar: string } };
type ObjectB = { foo: { bar: string, baz: string } };
let objectB: ObjectB = { foo: { bar: '123', baz: '456' } };
let objectA: ObjectA = objectB; // Error! Why?
Any ideas why?
Upvotes: 3
Views: 122
Reputation: 3478
At a high level, you can either reuse the objectB
instance with slight modification to the ObjectA type or you can create a new object. Let's dig into your options:
What you're trying to do is to cast an ObjectB
to an ObjectA
. The reason Flow is complaining is that the type of foo
in ObjectA
is invariant by default. The foo
property of ObjectB
is a subtype of the foo
property of ObjectA
. To get Flow to understand this, we just need to mark the foo
property as covariant:
(Try)
// @flow
type ObjectA = { +foo: { bar: string } };
type ObjectB = { foo: { bar: string, baz: string } };
let objectB: ObjectB = { foo: { bar: '123', baz: '456' } };
let objectA: ObjectA = objectB; // Woohoo, no error
Marking the property as "covariant" basically says that you promise to only read that property, you won't write to it. See, if you deleted the baz
property of objectA it would remove baz
from objectB. By marking it as covariant, Flow will throw an error if you write to it:
(Try)
// @flow
type ObjectA = { +foo: { bar: string } };
type ObjectB = { foo: { bar: string, baz: string } };
let objectB: ObjectB = { foo: { bar: '123', baz: '456' } };
let objectA: ObjectA = objectB;
objectA.foo = {bar: 'oh-oh, deleted baz in objectB'}; //Error
This pattern also works for deeper-nested objects:
(Try)
// @flow
type ObjectA = { +foo: { +bar: { baz: string } } };
type ObjectB = { foo: { bar: { bax: string, baz: string } } };
let objectB: ObjectB = { foo: { bar: { bax: '123', baz: '456' } } };
let objectA: ObjectA = objectB; // Woohoo, no error
See the Flow docs on depth subtyping for more details about this typing.
$ReadOnly<T>
Flow has a utlity type, $ReadOnly<T>
to mark all properties of an object as covariant, so you might want to use that instead:
(Try)
// @flow
type ObjectA = $ReadOnly<{ foo: { bar: string } }>;
type ObjectB = { foo: { bar: string, baz: string } };
let objectB: ObjectB = { foo: { bar: '123', baz: '456' } };
let objectA: ObjectA = objectB; // Woohoo, no error
Or you can create a ReadOnly instance of ObjectA and leave the ObjectA definition alone:
(Try)
// @flow
type ObjectA = { foo: { bar: string } };
type ObjectB = { foo: { bar: string, baz: string } };
let objectB: ObjectB = { foo: { bar: '123', baz: '456' } };
let objectA: $ReadOnly<ObjectA> = objectB; // Woohoo, no error
Alternatively, you can create a new copy of the object with a spread and avoid all this typing:
(Try)
// @flow
type ObjectA = { foo: { bar: string } };
type ObjectB = { foo: { bar: string, baz: string } };
let objectB: ObjectB = { foo: { bar: '123', baz: '456' } };
let objectA: ObjectA = {...objectB} // Create a new object
But that will only work one level deep and it creates an additional object. Typically I skip this option and end up using $ReadOnly<T>
when I need to use an object in a read-only manner.
Upvotes: 1
Reputation: 111
As far as I know, one possible approach is to spread out the object. Without this flow is not able to read the property.
// @flow
type ObjectA = { foo: { bar: string } };
type ObjectB = { foo: { bar: string, baz: string } };
let objectB: ObjectB = { foo: { bar: '123', baz: '456' } };
let objectA: ObjectA = {...objectB}; // spread operator applied
Upvotes: 0