Open meandmax opened 8 years ago
This seems to work for us so far but we are not sure if we doing this right. Immutable.js from FB has an existing module declaration but certainly immutable.js is not an option for us ...
https://github.com/TechnologyAdvice/flow-interfaces/blob/master/interfaces/immutable.d.js
declare module 'seamless-immutable' {
declare class SeamlessImmutable {
static (collection: any, prototype?: Object, depth?: number): any,
/* array functions */
flatMap(fn: Function): Array<any>,
asObject(fn: Function): Object,
asMutable(): Array<any>,
/* object functions */
merge(collection: Array<any> | Object, deep?: Object): Object,
set(key: string, value: any): Object,
setIn(keyPath: Array<string>, value: any): Object,
update(key: string, fn: Function): Object,
updateIn(keyPath: Array<string>, fn: Function): Object,
without(fn: Function): Object,
without(keys: Array<string>): Object,
without(...keys: Array<string>): Object,
asMutable(): Array<any> | Object
}
declare var exports: typeof SeamlessImmutable;
}
Hm, here's what I came up with so far (to cover only few methods I had):
declare class ImmutableArray<T> extends Array {
concat(item: (T | Array<T>)): ImmutableArray<T>;
slice(idx: number, len: ?number): ImmutableArray<T>;
updateIn<U>(keyPath: Array<string | number>, func: (item: U) => U): ImmutableArray<T>;
update(key: (string | number), func: (item: T) => T): ImmutableArray<T>;
}
so that:
const arr: ImmutableArray<string> = Immutable(['a', 'b']);
arr.slice(0).concat('c').concat(['d']).update(2, s => s); // no errors here
works.
Made them in like 5 minutes, someone might find it useful to implement proper e2e declarations, just like for lodash
.
I might try to convert that properly to declaration and send over a PR or just include as a 3rd party package. Would you be interested in helping out @meandmax?
@meandmax Do you mind if I use your definitions as the basis of a PR against seamless-immutable? I think it would make seamless-immutable more attractive if it supported flow out of the box.
I have played around with this a bit more. I have had some success with an intersection-based declaration.
// libs/seamless-immutable.d.js
declare module 'seamless-immutable' {
declare type Immutable<T: Object | Array<*>> = T & {
// Array methods
flatMap(fn: Function): Array<any>;
asObject(fn: Function): Object;
asMutable(): Array<any>;
// Object methods
merge(collection: Array<any> | Object, deep?: Object): Object;
set(key: string, value: any): Object;
setIn(keyPath: Array<string>, value: any): Object;
update(key: string, fn: Function): Object;
updateIn(keyPath: Array<string>, fn: Function): Object;
without(fn: Function): Object;
without(keys: Array<string>): Object;
without(...keys: Array<string>): Object;
asMutable(): Array<any> | Object;
};
declare function from<T: Object | Array<*>> (spec: T): Immutable<T>;
declare type Default = {
from: typeof from;
isImmutable: (x: *) => boolean
};
declare module.exports: Default;
}
@grabbou @ajhyndman Can you explain to a newbie how to use this?
I am completely new to flow, and I am unfortunately blocked by seamless-immutable. I really just want seamless tow work with flow.
Hey @MoeSattler,
You should be able to take one of these declarations, copy it into a file (named something like seamless-immutable.flow.js
) and drop it into a flow/libs/
(or any other name) directory inside your project. You can then add a reference to that directory under the [libs]
heading in your .flowconfig file.
If I didn't explain that well enough, there are full directions for adding declaration files here: https://flowtype.org/docs/declarations.html#pointing-your-project-to-declarations
@ajhyndman Thanks! done that. So how do I declare my data as Immutable? Just adding it to [libs] doesn't seem to fix it (used your solution)
@MoeSattler Calling Immutable.from()
on your data should now return an instance of an Immutable<-your-object-type->
which has methods available on it.
That said, I'm not a huge fan of my declaration. I've recently been experimenting with pretending the seamless-immutable methods don't exist and using another library like Ramda to do my data manipulation.
@ajhyndman Couldn't you in that case just use Object.freeze() instead of seamless?
You could implement something a bit cheaper, yep! I haven't bothered extracting the exception handling and recursive logic yet (or looking for something similar).
Seamless-immutable is a pretty small, single file anyway. If you find something great, I'd be keen to hear about it.
@ajhyndman https://github.com/scottcorgan/immu
Oh, I like the list of similar libraries they link in the readme, too. Thanks!
Would anyone be interested in contributing their definitions to https://github.com/flowtype/flow-typed?
This is the latest iteration of my version:
https://github.com/ajhyndman/fantasia/blob/master/flow/libs/seamless-immutable.d.js
Feel free to use it to make a PR against the flow-typed
repo.
With ES modules support:
/* @flow */
declare module 'seamless-immutable' {
declare type fromType = Object | Array<*>;
declare export type Immutable<T: fromType> = T & {
// Array methods
flatMap(fn: Function): Array<any>;
asObject(fn: Function): Object;
asMutable(): Array<any>;
// Object methods
merge(collection: Array<any> | Object, deep?: Object): Object;
set(key: string, value: any): Object;
setIn(keyPath: Array<string>, value: any): Object;
update(key: string, fn: Function): Object;
updateIn(keyPath: Array<string>, fn: Function): Object;
without(fn: Function): Object;
without(keys: Array<string>): Object;
without(...keys: Array<string>): Object;
asMutable(): Array<any> | Object;
};
declare export function from<T: fromType> (spec: T): Immutable<T>;
declare export function isImmutable(x: *): boolean;
declare export default typeof from;
}
I've been trying to improve upon these a bit to allow type aliasing of the immutable object itself for things like redux state and to restrict inputs to merge,
update,
without` to valid objects/keys. Still very much a work in progress but this improves things for simple objects:
/* @flow */
declare module 'seamless-immutable' {
declare type fromType = Object | Array<*>;
declare export type Immutable<T: fromType> = T & {
// Array methods
flatMap(fn: Function): Array<any>;
asObject(fn: Function): Object;
asMutable(): Array<any>;
// Object methods
merge(collection: Array<$Shape<T>> | $Shape<T>, deep?: Object): Object;
set(key: $Keys<T>, value: any): Object;
setIn(keyPath: Array<string>, value: any): Object;
update(key: $Keys<T>, fn: Function): Object;
updateIn(keyPath: Array<string>, fn: Function): Object;
without(fn: Function): Object;
without(keys: Array<$Keys<T>>): Object;
without(...keys: Array<$Keys<T>>): Object;
asMutable(): Array<any> | Object;
};
declare export function from<T: fromType> (spec: T): Immutable<T>;
declare export function isImmutable(x: *): boolean;
declare export default {
from<T: fromType>(spec: T): Immutable<T>,
isImmutable(x: *): boolean,
};
}
Usage is something like:
import Immutable from 'seamless-immutable';
import type {Immutable as ImmutableType} from 'seamless-immutable';
type MyState = {
requiredProp: string,
optionalProp?: number,
};
const immutableState: ImmutableType<MyState> = Immutable.from(
({requiredProp: "foo"}: MyState)
);
immutableState.merge({optionalProp: 42}); // no error
immutableState.merge({optionalProp: "42"}); // ERROR
immutableState.merge({badProp: "other"}); // ERROR
immutableState.set('badProp', 42); // ERROR
Still has many, many shortcomings, the updateIn or setIn keypaths are not validated, nor are the values passed to set
or returned from the update
function. Removing required properties using without
is also possible - would likely have to two generic types similar to Props and RequiredProps for that to work.
@azundo Cool!
Have you considered trying to upgrade from the intersection type to a type spread? That syntax wasn't present in Flow at the time I took a stab at this before, but should model what's happening a bit better.
i.e.
declare export type Immutable<T: fromType> = {
...T;
// Array methods
flatMap(fn: Function): Array<any>;
// ...
@ajhyndman Ah, yep, that makes sense. Unfortunately it seems like $Shape doesn't do deep matching so my updates are less useful than I was hoping as type checking fails for deep merging: https://github.com/facebook/flow/issues/2542
@azundo Found this...
https://gist.github.com/mizchi/3054bca701dff4b5e28efd9133c66818
seems to be working great for me thus far.
import Immutable, { type Immutable as ImmutableType } from 'seamless-immutable'
type LoginState = {
authToken: string,
error: any,
fetching: boolean,
loading: boolean,
}
type _LoginState = ImmutableType<LoginState>
const INITIAL_STATE: _LoginState = Immutable({
authToken: '',
error: null,
fetching: false,
loading: false,
})
export const request = (state: _LoginState): _LoginState =>
state.merge({ fetching: true })
Awesome, thanks @chrisbull will check it out. The operation I couldn't get to typecheck was deep merging. Something like:
type NestedState = {
propA: string,
propB: string,
}
type TopLevelState = {
otherProp: string,
nested: NestedState,
}
const state = ImmutableType<TopLevelState> = Immutable.from({
otherProp: 'bar',
nested: {
propA: 'A',
propB: 'B',
},
});
state.merge({nested: {propB: 'C'}}, {deep: true}); // should pass
state.merge({nested: {propC: 'C'}}, {deep: true}); // should fail
Had a hard time finding anything that would meet both of those conditions for arbitrarily nested states.
I tried out some of the example type definitions here but none of them seem to work properly for arrays. Is no one using seamless immutable for arrays as well?
I'm working on some more advanced typing for a project now and will share when I get further along.
@azundo So first, technically your state.merge({nested: {propB: 'C'}}, {deep: true});
would always pass, as you are Immutable allows you to merge new items into your object called nested. What it doesn't allow you to do is change nested
to an different kind of value, for example an array or a number. So state.merge({nested: [1,2,3]})
should fail.
That said, here is an updated version of how I am managing and updating Flow Types:
/* @flow */
import * as Immutable from 'seamless-immutable'
// Reusable Types
export type ImmutableStateType<S> = Immutable.ImmutableObjectMixin<S>
export type ActionType<T> = T & { type?: string, payload?: T }
export type ReducerType<S, A> = (state: S, action: A) => S
// Specific Types - this would be reducer specific
export type MyNestedState = {
otherProp?: string,
nested?: {
propA?: string,
propB?: string,
},
}
export type MyNestedImmutableState = ImmutableStateType<MyNestedState>
export type MyNestedAction = ActionType<MyNestedState> // return { type?: string, payload?: MyNestedState }
export type MyNestedActionWithAddedProps = MyNestedAction & {
propA?: string,
propB?: string,
otherProp?: string,
} // return { type?: string, payload?: MyNestedState, propA?: string, propB?: string, otherProp?: string }
export type MyNestedReducer = ReducerType<MyNestedImmutableState, MyNestedActionWithAddedProps>
/* ------------- Initial State ------------- */
const initState: MyNestedState = {
otherProp: 'bar',
nested: {
propA: 'A',
propB: 'B',
},
}
export const INITIAL_STATE: MyNestedImmutableState = Immutable.Immutable(initState)
/* ------------- Reducers ------------- */
// User
export const mergePropA: MyNestedReducer = (state, { otherProp, propA }) =>
state.merge({
otherProp,
nested: {
...state.nested,
propA,
},
}) // should pass
export const mergePropB: MyNestedReducer = (state, { otherProp, propB }) =>
state.merge({
otherProp,
nested: {
...state.nested,
propB,
},
}) // should pass
export const mergePropC: MyNestedReducer = (state, { otherProp, propC }) =>
state.merge({
otherProp,
nested: {
...state.nested,
propC,
},
}) // should fail
@akread Hopefully this will help you as well.
@chrisbull Since immutable allows merging arbitrary values it would be great for the type system to be able to catch misnamed keys (propC
in my example). If my immutable state is of type Immutable<TopLevelState>
then the return type of Immutable<TopLevelState>.merge
should also be Immutable<TopLevelState>
which is what I haven't been able to accomplish.
@azundo I should have mentioned this before. I'd also recommend making the Object in the nested
it's own Type ... for example type NestedObjType = { propA: string, propB: string }
and then include it in the nested: NestedObjType
that way, nested always has to receive an object that is of type NestedObjType
type NestedObjType = {
propA: string,
propB: string
}
type ImmutableNestedObj = ImmutableStateType<NestedObjType>
type MyNestedState = {
otherProp?: string,
nested?: ImmutableNestedObj,
}
const INITIAL_NESTED_OBJ: NestedObjType = {
propA: 'A',
propB: 'B',
}
const initialNestedObj: ImmutableNestedObj = Immutable.Immutable(INITIAL_NESTED_OBJ)
const initState: MyNestedState = {
otherProp: 'bar',
nested: initialNestedObj,
}
export const INITIAL_STATE: MyNestedImmutableState = Immutable.Immutable(initState)
Also to note, I'm not a contributor to this project. I'm just a contract developer learning this all as I go as well, but just wanted to help if I could.
I'd love to hear if anyone else a better way of doing this.
@chrisbull my point was that these type definitions don't work well for arrays – only objects. They also don't support adding prototype methods on initialization with seamless-immutable. I'm working on a more comprehensive library type definition based on the one that you found that includes support for arrays, prototype methods, as well as key/value validation.
Maybe I'm missing something on the array side? It didn't really work as expected when I tried.
I've included some basic testing in my app to validate the typing but I'd love to share it once it is done and get some outside input. Ideally we can get some form of library type definition into the source code here for everyone to use. I can post it here and make a PR for a more in-depth review when it is complete.
Anybody an idea how to write a module declaration for this or how to use seamless with flow type? Any recommendations? Where to start, here we tried and failed badly a couple of times ...