nariel0822
nariel0822

Reputation: 75

Flow error with deep subtypes

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

Answers (2)

James Kraus
James Kraus

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:

Marking an object property as covariant

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.

Using $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

Creating a new object

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

Harsh Vardhan
Harsh Vardhan

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

Related Questions