-
-
Save glenjamin/75a96b45f4bb5c6ac221815d28c548dd to your computer and use it in GitHub Desktop.
/* @flow */ | |
import * as I from "immutable"; | |
/** | |
* Define an immutable record intended for holding reducer state | |
* @param spec - the keys and their default values | |
* @return a state record factory function | |
*/ | |
export function defineRecord<T: Object>( | |
name: string, | |
spec: T | |
): (init: $Shape<T>) => Record<T> { | |
return I.Record(spec, name); | |
} | |
export type Record<T: Object> = RecordMethods<T> & T; | |
declare class RecordMethods<T: Object> { | |
get<A>(key: $Keys<T>): A; | |
set<A>(key: $Keys<T>, value: A): Record<T>; | |
update<A>(key: $Keys<T>, updater: (value: A) => A): Record<T>; | |
updateIn<A>(path: Iterable<any>, notSetOrUpdater: A | (value: A) => A, updater?: (value: A) => A): Record<T>; | |
setIn<A>(path: Iterable<any>, value: A): Record<T>; | |
deleteIn<A>(path: Iterable<any>): Record<T>; | |
merge(values: $Shape<T>): Record<T>; | |
inspect(): string; | |
toObject(): T; | |
// add more as needed | |
} |
@glenjamin I've settled on a different approach, extending the defined Record class. The elegant part is that you don't have to have a separate ThingRecord
type, instead you just have a Thing
class. The inelegant part is that there's more redundant field declaration. I also feel a lot more comfortable with this approach because I've gotten the impression that Flow intersection type validation still needs a lot of work, from the heady errors I've seen...
// @flow
import {Record as iRecord} from 'immutable'
export interface RecordAPI<T: Object> {
constructor(init?: $Shape<T>): void;
get<A>(key: $Keys<T>): A;
set<A>(key: $Keys<T>, value: A): Record<T>;
hasIn(keys: Array<any>): boolean;
update<A>(key: $Keys<T>, updater: (value: A) => A): Record<T>;
updateIn<A>(path: Array<any>, updater: (value: A) => A): Record<T>;
merge(values: $Shape<T>): Record<T>;
withMutations(mutator: (mutable: Record<T>) => any): Record<T>;
inspect(): string;
toObject(): T;
toJS(): Object;
}
export default function Record<T: Object>(spec: T): Class<RecordAPI<T>> {
return iRecord(spec)
}
type ThingFields = {
id: number,
name: string,
}
const thingDefaults: ThingFields = {
id: 0,
name: '',
}
class Thing extends Record(thingDefaults) {
id: number
name: string
}
const thing: Thing = new Thing({name: 'blah'})
One can reduce the boilerplate a little bit by doing (imagine these are non-primitive fields that wouldn't get inferred correctly from the default values):
const thingDefaults = {
id: (0: number),
name: ('': string),
}
class Thing extends Record(thingDefaults) {
id: number
name: string
}
const thing: Thing = new Thing({name: 'blah'})
@jedwards1211 One issue with your formulation is that this doesn't type check:
const thing: Thing = new Thing({name: 'blah'});
const thing2: Thing = thing.set('name', 'blah2');
Namely, I believe the return type of set
is Record<ThingFields>
where as it really should be Thing
.
More broadly, the other problem is that this does type check:
const thing: Thing = new Thing({name: 'blah'});
const thing2 = thing.set('name', 5);
I'm not sure how to solve either of these problems. :-/
It looks like one can use $Subtype<T>
to make my first example compile:
export interface RecordAPI<T: Object> {
// ...
set<A>(key: $Keys<T>, value: A): $Subtype<Record<T>>;
// ...
}
This now works:
const thing: Thing = new Thing({name: 'blah'});
const thing2: Thing = thing.set('name', 'blah2');
I don't think my other problem can be fixed in Flow as it is now.
@jedwards1211 - I like your approach, but it's a shame that you have to declare all the fields twice.
Also, but how do you organize your code? Do you put the RecordAPI
code somewhere likelib
, and import it? And do you create a folder for all the records, such as models
or records
? Or do you just define each Record inside the relevant reducer, and export it from there?
@glenjamin in your example, I'm struggling to understand how you use all of the things that are exported:
export type ThingShape
export type ThingRecord
export const Thing
const thing: ThingRecord = Thing({name: "blah"});
So in that example, thing
has a type of ThingRecord
, and so I guess Thing
is just a function that returns something with a type of ThingRecord
.
Ah, I think I get it. But I think it might be clearer if Thing
was renamed to createThingRecord
:
export const createThingRecord = defineRecord("Thing", ({
id: "",
name: "",
}: ThingShape));
Just to clarify that you're not creating a new Thing
class, you're calling a plain function that returns a ThingRecord
.
We've been able to get record flow types to work using the newest v4.0.0-RC-2 release and a bug fix to the record types. With these fixes we can do the following:
// create new record "class"
const newRecord = Record({ id: 0, name: '' });
// create a dummy instance of that record to use for typing
const dummyInst = newRecord();
// create the type to use when declaring the interface to a component
type recordInterface = typeof dummyInst;
// export the record "class" to be used for record instance creation
export { newRecord };
When a record instance is created and passed around, the type of the record is actually typeof dummyInst
not newRecord
. So in many cases, you create a dummy instance to generate the correct type, even though it may not be used anywhere in the actual javascript.
@ianwcarlson
I've tried to fork Immutable repository and apply mentioned bugfix. It doesn't seem to work.
const Person = Record({
name: null,
age: 0,
isAdult: false,
})
const personInstance = Person()
type TPerson = typeof personInstance
const Animal = Record({
name: null,
owner: null,
})
const animalInstance = Animal()
type TAnimal = typeof animalInstance
export const checkAge = (person: TPerson): void => {
if (person.age >= 18) {
console.log('ADULT')
} else {
console.log('CHILD')
}
}
export const foobar = () => {
const person = Person()
const animal = Animal()
checkAge(animal)
}
In this example, the Flow doesn't detect wrong type of object passed to checkAge
function. It doesn't event detect if I pass a native type there: checkAge(true)
.
Am I doing something wrong?
I'm also not able to get @ianwcarlson fix working. Has anyone been successful ?
This was updated on the 2nd of August to use a class, which seems to make flow much faster.