Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Record Flow type improvements #1870

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions type-definitions/flow-tests/.flowconfig
Expand Up @@ -7,3 +7,6 @@ module.name_mapper='^immutable$' -> '../../type-definitions/immutable.js.flow'
[ignore]
💩 Only interested in testing these files directly in this repo.
.*/node_modules/.*

[lints]
deprecated-type=error
16 changes: 8 additions & 8 deletions type-definitions/flow-tests/immutable-flow.js
Expand Up @@ -54,27 +54,27 @@ const ImmutableMap = Immutable.Map;
const ImmutableStack = Immutable.Stack;
const ImmutableSet = Immutable.Set;
const ImmutableKeyedCollection: KeyedCollection<
*,
*
mixed,
mixed
> = Immutable.Collection.Keyed();
const ImmutableRange = Immutable.Range;
const ImmutableRepeat = Immutable.Repeat;
const ImmutableIndexedSeq: IndexedSeq<*> = Immutable.Seq.Indexed();
const ImmutableIndexedSeq: IndexedSeq<mixed> = Immutable.Seq.Indexed();

const Immutable2List = Immutable2.List;
const Immutable2Map = Immutable2.Map;
const Immutable2Stack = Immutable2.Stack;
const Immutable2Set = Immutable2.Set;
const Immutable2KeyedCollection: Immutable2.KeyedCollection<
*,
*
mixed,
mixed
> = Immutable2.Collection.Keyed();
const Immutable2Range = Immutable2.Range;
const Immutable2Repeat = Immutable2.Repeat;
const Immutable2IndexedSeq: Immutable2.IndexedSeq<*> = Immutable2.Seq.Indexed();
const Immutable2IndexedSeq: Immutable2.IndexedSeq<mixed> = Immutable2.Seq.Indexed();

var defaultExport: List<*> = Immutable.List();
var moduleExport: List<*> = Immutable2.List();
var defaultExport: List<mixed> = Immutable.List();
var moduleExport: List<mixed> = Immutable2.List();

var numberList: List<number> = List();
var numberOrStringList: List<string | number> = List();
Expand Down
54 changes: 32 additions & 22 deletions type-definitions/flow-tests/record.js
Expand Up @@ -14,11 +14,9 @@ const Point3: RecordFactory<{ x: number, y: number, z: number }> = Record({
type TGeoPoint = { lat: ?number, lon: ?number };
const GeoPoint: RecordFactory<TGeoPoint> = Record({ lat: null, lon: null });

// TODO: this should be FlowExpectedError - 'abc' is not a number
// However, due to support for the brittle support for subclassing, Flow
// cannot also type check default values in this position.
const PointWhoops: RecordFactory<{ x: number, y: number }> = Record({
x: 0,
// $FlowExpectedError[incompatible-type-arg]
y: 'abc',
});

Expand Down Expand Up @@ -146,14 +144,15 @@ const mistakeNewInstance = new MakePointNew({ x: 'string' });
// Note use of + for Read Only.
type TPerson = { +name: string, +age: number };
const defaultValues: TPerson = { name: 'Aristotle', age: 2400 };
const PersonRecord = Record(defaultValues);
const PersonRecord = Record<TPerson>(defaultValues);

class Person extends PersonRecord<TPerson> {
class Person extends PersonRecord {
getName(): string {
return this.get('name');
}

setName(name: string): this & TPerson {
// $FlowIssue[incompatible-return]
return this.set('name', name);
}
Comment on lines 154 to 157
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new changes, Flow doesn't like the this & TPerson return type one bit:

Error ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ type-definitions/flow-tests/record.js:155:12

Cannot return this.set(...) because: [incompatible-return]
 • Either RecordInstance [1] is incompatible with this [2].
 • Or TPerson [3] is incompatible with this [2].

     type-definitions/flow-tests/record.js
 [3]  147│ const PersonRecord = Record<TPerson>(defaultValues);
         :
      152│   }
      153│
 [2]  154│   setName(name: string): this & TPerson {
      155│     return this.set('name', name);
      156│   }
      157│ }
      158│

     type-definitions/immutable.js.flow
 [1] 1659│   set<K: $Keys<T>>(key: K, value: $ElementType<T, K>): this & $ReadOnly<T>;



Found 1 error

}
Expand All @@ -170,23 +169,6 @@ person.get('unknown');
// $FlowExpectedError[prop-missing]
person.set('unknown', 'Thales');

// Note: not <TPerson>
Copy link
Author

@mwiencek mwiencek Jul 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the use case here is (and PersonRecord is no longer parameterized anyway) so I removed the test altogether. If someone can explain the use case I can perhaps update it instead. :)

Edit: To be clear, the test right above this one is now class Person extends PersonRecord, so it's "without types" (though that isn't really possible anymore) now, and the .get('unknown'); there errors, which I think is correct, but which contradicts this test (at "Note: no error").

class PersonWithoutTypes extends PersonRecord {
getName(): string {
return this.get('name');
}

setName(name: string): this & TPerson {
return this.set('name', name);
}
}

const person2 = new PersonWithoutTypes();

person2.get('name');
// Note: no error
person2.get('unknown');


// Functional Merge

Expand Down Expand Up @@ -215,3 +197,31 @@ const record = xyRecord();
(merge(record, Map([['z', 123]])): XYPointRecord);
// $FlowExpectedError[incompatible-call]
(merge(record, List([123])): XYPointRecord);


// Type inference (issue #1722)

type Product = { address: string };
type App = { name: string };
const AppDefaults: App = { name: '' };

const AppRecordImplicit = Record(AppDefaults);
const AppRecordExplicit = Record<App>({ name: '' });
// $FlowExpectedError[prop-missing]
const AppRecordBorked: RecordFactory<App> = Record<Product>({ address: '' });

const appImplicit = AppRecordImplicit({ name: 'i' });
(appImplicit.get('name'): string);
(appImplicit.name: string);
// $FlowExpectedError[incompatible-call]
appImplicit.get('address');
// $FlowExpectedError[incompatible-use]
appImplicit.address;

const appExplicit = AppRecordExplicit({ name: 'e' });
(appExplicit.get('name'): string);
(appExplicit.name: string);
// $FlowExpectedError[incompatible-call]
appExplicit.get('address');
// $FlowExpectedError[incompatible-use]
appExplicit.address;
11 changes: 6 additions & 5 deletions type-definitions/immutable.js.flow
Expand Up @@ -1583,17 +1583,18 @@ type RecordOf<Values: Object> = RecordInstance<Values> & $ReadOnly<Values>;

// The values of a Record instance.
type _RecordValues<T, R: RecordInstance<T> | T> = R;
type RecordValues<R> = _RecordValues<*, R>;
type RecordValues<R> = _RecordValues<mixed, R>;

declare function isRecord(
maybeRecord: any
): boolean %checks(maybeRecord instanceof RecordInstance);
declare class Record {
static <Values: Object>(spec: Values, name?: string): typeof RecordInstance;
constructor<Values: Object>(

declare class Record<Values: Object> {
static (spec: Values, name?: string): RecordFactory<Values>;
constructor(
spec: Values,
name?: string
): typeof RecordInstance;
): RecordFactory<Values>;

static isRecord: typeof isRecord;

Expand Down