Unsealed objects

Tl;dr: they can be tricky.

Unsealed objects are a special case in Flow. They allow you to create an empty object to be able to add properties later (see the docs). There are some special properties worth mentioning:

  1. reading unknown property from unsealed objects is unsafe
const obj = {};
obj.foo = 1;
obj.bar = true;
const foo: number = obj.foo; // Works!
const bar: boolean = obj.bar; // Works!
const baz: string = obj.baz; // Works? (reads from unsealed objects with no matching writes are never checked)
const fail: string = obj.foo; // Errors correctly.
  1. you cannot assign unsealed object to the exact type
type Foo = {| a?: string, b?: string |};
const foo1: Foo = { a: '' }; // works as expected
const foo2: Foo = {}; // doesn't work, but should (?)
const foo3: Foo = { ...null }; // this is equivalent to {} but is not an unsealed object (it's sealed)
const foo4: Foo = Object.freeze({}); // alternatively, seal the object manually

Gotchas

const a: { foo: string } = {}; // No error?! ❌
const b: { foo: string } = { ...null }; // Error βœ…
const x: {} = ''; // Error βœ…
type Record = {
foo: number,
bar: string,
};
const x: Record = {}; // No error?! ❌

https://github.com/facebook/flow/issues/8091

There is also a know bug with unsealed objects vs. optional chaining lint:

// @flow strict
type State = {|
base?: {|
data: {|
a: string,
|},
|},
|};
const getBase = (state: State) => state.base ?? {};
// Fixed version:
// const getBase = (state: State) => state.base ?? { data: undefined };
export const getA = (state: State) => getBase(state).data?.a;

This core results incorrectly in:

Error β”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆ src/optional-chain-flow.js:16:39
This use of optional chaining (?.) is unnecessary because getBase(...).data [1] cannot be nullish or because an earlier
?. will short-circuit the nullish case. (unnecessary-optional-chain)
13β”‚ // Fixed version:
14β”‚ // const getBase = (state: State) => state.base ?? { data: undefined };
15β”‚
[1] 16β”‚ export const getA = (state: State) => getBase(state).data?.a;
17β”‚
Found 1 error

See: https://github.com/facebook/flow/issues/8065

Note on Object type

This is most probably not really about sealed types but it feels somehow related.

This is fine:

const test: { a: string } = {};
const x = test.a; // βœ…
test.a = '2'; // βœ…

But you cannot read/write from an empty Object type

const test: {} = {};
const x = test.a; // ❌
test.a = 2; // ❌

Previously, Object type was internally defined like { [key: string]: any } which implies what you can do with it:

const test: { [key: string]: any } = {};
const x = test.a; // βœ…
test.a = '2'; // βœ…
const test: { [key: string]: any } = {};
const x = test.a; // βœ…
test.a = 2; // βœ…