I have being wondering how one could annotate set, update or merge style functions on immutable records found in Immutable.js
In fact I find I'm not sure how immutable data structures can at all type checked with flow given that there are no syntax for it in JS so the best I could come up with was something along these lines:
var set <Record:Object> = (key:string, value:any, record:Record) => Record;
var update <Record:Object> = (key:string, f:(value:any) => any) => Record;
var merge <Record:Object, Overrides:Object> => (overrides:Overrides:, record:Record) => Record;
But than neither key or value seems to be restricted to the the Record keys / value types of the Record type. I imagine if flow is given the source for these functions it maybe able to infer that invalid key would invalidate return type constraint and there for fail to type check. Although it would be better to be able to express this with a type signature itself in case there is no typed implementation available for example Object.assign is one example of that.
I am also unsure how that would even work if these functions were defined on prototype I imagine something like this:
interface Record <fields:Object> {
set(key:string, value:any) => Record<fields:Object>;
update <T> (key:string, f:(value:T) => T) => Record<fields>;
merge <T> (overrides:T) => Record<fields>;
}
But then I'm not sure how to say that this instance must be record that has fields type signature.
I've been trying to achieve similar things when trying to add types for https://github.com/rackt/redux, and I've just recently found that in the type declaration for React there's some possibly useful (internal) features to achieve that kind of type checks: https://github.com/facebook/flow/blob/master/lib/react.js#L25
Haven't tried them though, but if they work then probably could be more reason to make them part of the public API
@leoasis see #875
Ahhh, so we need to wait for it a bit more then :cry:
We've been kicking around this idea for a while now and I'm hoping to address some of this in the coming months. The much bigger/impactful change to improve Immutable.js types, supporting this types, has already landed. Helping Flow understand Immutable.Record in a type safe way is an important goal for me personally, FB internally, and I hope the JS community at large.
Watch this space for updates. Please don't +1—it's already prioritized. Just hit the subscribe button, please.
This might need to be a totally separate initiative, but how about an extension to flow and an accompanying Babel plugin to support data types that can be instantiated, whose properties would be immutable by default? For example:
data type User {
id: ?number,
mutable firstName: string,
mutable lastName: string,
mutable email: string,
mutable password: string,
//"mutable" here means only that you can set the address property to a
//different address; the address itself is immutable.
mutable address: ?Address
}
data type Address {
id: ?number,
streetAddress: string;
city: string;
stateOrProvince: StateOrProvince;
country: Country;
postalCode: PostalCode;
}
let john = new User({firstName:'John', lastName:'McCarthy'});
john.address = new Address({...});
john.address.city = 'New City'; //compile error; city is immutable
I'm thinking of this as being for objects that contain only data properties and no methods (if it were to support methods as well, then it would be getting into the same territory as classes and we don't need two separate constructs for that)...so in reality User might be a class while Address could be a data type. But mutable properties can still be useful for properties that can legitimately change, e.g. if a person gets a new email address or even changes their name, it's still the same person.
There should probably also be the ability to declare subtypes; maybe an "extends" keyword that specifies the prototype relationship as in classes.
Sorry if this is too off-topic for this thread. I posted it here because I think it's relevant to the big picture of how to better support the declaration of data types that are not necessarily mutable classes. I was thinking of maybe writing a Babel plugin for this but if it could be a joint effort with flow that would be great.
@mbrowne I already experimented with something like this: https://github.com/stephanos/babel-plugin-immutable-record - I haven't been able to use it a lot so far so consider this early-beta ;)
Nice. It doesn't support a mix of mutable and immutable properties like I was thinking of, but of course that can be accomplished with classes and perhaps it's more common for data types to be either entirely mutable or immutable. Or alternatively this could serve as a nice starting point if I decide to implement my syntax above - thanks for pointing it out.
Hey @samwgoldman, is this still being prioritized? Any update since a year ago?
@samwgoldman I don't understand how the Facebook developers use this in their daily life? You advocate the following three pretty heavily: react.js, flow, immutable.js. But the three can not work together successfully when you try to use the Record type of immutable.js, which for me is the most useful type. My guess is that you must have some kind of workaround so that your codebase typechecks. If so, could you please share your experience with us?
@hmeerlo I'm not at Facebook, but I struggled with this for a while to, and this is what I came up with:
/* @flow */
import * as Immutable from 'immutable';
/**
* This is a restricted typed interface to `immutable.Record`.
*
* Notably, the `get` methods is missing, because they expose
* holes in the type system. (So does `set` and `update`, but they
* are harder to avoid.)
*
* @private
*/
declare class IRecord<T: Object> {
/**
* Create a new typed immutable record with the values given.
*
* @memberof TypedRecord
* @param {$Shape<T>} values The values to initialize this record with.
* @returns {$Subtype<IRecord<T>>} The record instance.
*/
constructor(values: $Shape<T>): void;
/**
* @memberof TypedRecord
* @returns {string} A string representation of this record.
*/
inspect(): string;
/**
* Set the given key to the given value.
*
* @memberof TypedRecord
* @returns {$Subtype<IRecord<T>>} The updated record instance.
*/
set<A>(key: $Keys<T>, value: A): $Subtype<IRecord<T>>;
/**
* Update the given key to the value returned by the given function.
*
* @memberof TypedRecord
* @returns {$Subtype<IRecord<T>>} The updated record instance.
*/
update<A>(key: $Keys<T>, updater: (value: A) => A): $Subtype<IRecord<T>>;
/**
* @memberof TypedRecord
* @param {$Shape<T>} values
* Values to replace in the current record.
* @returns {$Subtype<Record<T>>}
* A new record with the given values.
*/
merge(values: $Shape<T>): $Subtype<IRecord<T>>;
/**
* Apply a batch of mutations to this record. This may be more efficient
* than calling `merge` multiple times.
*
* @memberof TypedRecord
* @param mutator
* A mutator function that accepts the current record and returns a
* new one.
* @returns {$Subtype<IRecord<T>>} The new record.
*/
withMutations(
mutator: (mutable: $Subtype<IRecord<T>>) => $Subtype<IRecord<T>>,
): $Subtype<IRecord<T>>;
/**
* Convert this record to a normal Javascript object.
*
* @memberof TypedRecord
* @return {Object} A plain Javascript object of this record.
*/
toJS(): Object;
}
/**
* Converts an `immutable.Record` specification to a class constructor for
* typed immutable records. The return value of this function is a **class**
* and not an instance of a class. The instance methods of the class returned
* are documented on this function.
*
* The parameter doubles as both the type specification for this record and
* as the initial default values used whenever a record is constructed.
*
* **Example usage:**
*
* ```
* class Foo extends TypedRecord({x: 0, y: 'foo'}) {
* x: number;
* y: string;
* }
*
* const foo1 = new Foo({x: 5});
* expect(foo1.x).toEqual(5);
* expect(foo1.y).toEqual('foo');
*
* // This won't type check.
* const foo2 = foo1.merge({x: 'not a number'});
* // Neither will this.
* const foo3 = new Foo({y: 5});
* ```
*
* @function
*/
export default function TypedRecord<T: Object>(spec: T): Class<IRecord<T>> {
return Immutable.Record(spec);
}
This lets you do things like
export Class Foo extends TypedRecord({
field1: 0,
field2: '',
}) {
field1: number;
field2: string;
}
where accessing the instance members field1 and field2 directly will have meaningful types (notice the lack of a get method in the IRecord class above). The merge function will be typed nicely as well, but the set and update functions still subvert the type system.
It's not perfect by any stretch, but this is the best I could come up with.
@BurntSushi Please accept my 'Hero of the day' award! Finally something that is quite useable considering the current shortcomings of flow.
@hmeerlo: you may also want to take a look at debugger.html's type for Record.
It's similar to what BurntSushi posted, but it doesn't use classes. I've been using it for a few weeks and it works fine, even though it feels a bit wonky.
I had a hard time wrapping my mind around using it for polymorphic records (that is, records which hold other records of any shape) or using that in a polymorphic function (which would take, for example, any record that has the field foo of type number).
Aaand the errors reported by Flow when you use this approach are not that straightforward, but other than that it's okay – well, it's better than having either no records or no types at all.
Is it unresolved yet?
I notice that $Shape is already used here:
https://github.com/facebook/immutable-js/blob/master/type-definitions/immutable.js.flow#L1146
@Gozala
Any news on this issue?
Is it possible to use Immutable and Flow together?
As long as you're on one of the release candidates for Immutable 4.0.0, it works pretty well with Flow right now. Check out the Record docs's "Flow Typing Records" section.
Most helpful comment
@hmeerlo I'm not at Facebook, but I struggled with this for a while to, and this is what I came up with:
This lets you do things like
where accessing the instance members
field1andfield2directly will have meaningful types (notice the lack of agetmethod in theIRecordclass above). Themergefunction will be typed nicely as well, but thesetandupdatefunctions still subvert the type system.It's not perfect by any stretch, but this is the best I could come up with.