rtfeldman / seamless-immutable

Immutable data structures for JavaScript which are backwards-compatible with normal JS Arrays and Objects.
BSD 3-Clause "New" or "Revised" License
5.37k stars 195 forks source link

Seamless-Immutable & Flow #121

Open meandmax opened 8 years ago

meandmax commented 8 years ago

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 ...

declare module 'seamless-immutable' {
  declare class SeamlessImmutableCollection {
    (collection: any, prototype?: Object, depth?: number): SeamlessImmutableCollection,

    /* 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: SeamlessImmutableCollection;
}
meandmax commented 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;
}
grabbou commented 8 years ago

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?

ajhyndman commented 8 years ago

@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.

ajhyndman commented 8 years ago

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;
}
ghost commented 8 years ago

@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.

ajhyndman commented 8 years ago

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

ghost commented 8 years ago

@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)

ajhyndman commented 8 years ago

@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.

ghost commented 8 years ago

@ajhyndman Couldn't you in that case just use Object.freeze() instead of seamless?

ajhyndman commented 8 years ago

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.

ghost commented 8 years ago

@ajhyndman https://github.com/scottcorgan/immu

ajhyndman commented 8 years ago

Oh, I like the list of similar libraries they link in the readme, too. Thanks!

rgbkrk commented 7 years ago

Would anyone be interested in contributing their definitions to https://github.com/flowtype/flow-typed?

ajhyndman commented 7 years ago

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.

KROT47 commented 7 years ago

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;
}
azundo commented 6 years ago

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.

ajhyndman commented 6 years ago

@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>;
    // ...
azundo commented 6 years ago

@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

chrisbull commented 6 years ago

@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 })
azundo commented 6 years ago

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.

akread commented 6 years ago

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.

chrisbull commented 6 years ago

@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.

azundo commented 6 years ago

@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.

chrisbull commented 6 years ago

@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.

akread commented 6 years ago

@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.