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

Add TypeScript typings #21

Closed
christianchown opened this issue Nov 9, 2018 · 96 comments
Closed

Add TypeScript typings #21

christianchown opened this issue Nov 9, 2018 · 96 comments

Comments

@christianchown
Copy link
Collaborator

@christianchown christianchown commented Nov 9, 2018

I'm happy to do if you don't use TypeScript. I've begun a WIP, but have a way to go before it's in usable form. It may take me a bit of time to fit in around my other work....

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Nov 9, 2018

Would certainly be awesome!

No rush at all. Let's keep this issue open and perhaps we can get some more volunteers to help. 👍

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Nov 9, 2018

FYI, I've been really stoked on all of your contributions and have sent you an invite to be a collaborator on this project. 👍

@aalpgiray

This comment has been minimized.

Copy link

@aalpgiray aalpgiray commented Nov 12, 2018

Tried and failed. :) Have anyone started yet?

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 12, 2018

@aalpgiray - I've started, not there yet though

@m5r

This comment has been minimized.

Copy link
Contributor

@m5r m5r commented Nov 17, 2018

Great idea, let us know how can we help!

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 17, 2018

This is where I'm stuck - https://www.reddit.com/r/reactjs/comments/9xni15/what_did_everyone_work_on_this_week_rreactjs/e9w586c/

I have hope that type-zoo may unlock the last few pieces

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 18, 2018

I have a working set of types: https://github.com/christianchown/easy-peasy/blob/typescript-typings/index.d.ts

Anyone know of a guide/best practise on annotating typings via comments, and adding tests for them, as these are way more complex than anything I've done in Typescript before...

@aalpgiray

This comment has been minimized.

Copy link

@aalpgiray aalpgiray commented Nov 20, 2018

@christianchown. Does definitely typed provide a sufficient guide ?

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Nov 20, 2018

Phew! @christianchown you look to be a Typescript god. That is some impressive typing going on. Excited about this. 👍

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 20, 2018

It's not far off being ready for prime time, I think. Some examples...

import { createStore, effect, select, Action, Effect, Select } from 'easy-peasy';

interface TodoValues {
  items: Array<string>;
}

interface TodoActions {
  saveTodo: Effect<Model, string>;
  todoSaved: Action<TodoValues, string>;
  lengthOfItems: Select<TodoValues, number>;
}

interface Model {
  todos: TodoValues & TodoActions;
}

const store = createStore<Model>({
  todos: {
    items: [],

    saveTodo: effect(async (dispatch, payload, getState) => {
      const saved = await todoService.save(payload);
      dispatch.todos.todoSaved(saved);          //  👍 correctly typed
      // dispatch.todos.todoSaved(1);               👍 correctly errors! (1 is not assignable to string)
      // dispatch.notToDos.something();             👍 correctly errors! (notToDos does not exist on Dispatch<StoreState>)
      if (getState().todos.lengthOfItems > 10) { // 👍 correctly typed
        await todoService.reportBigUsage();
      }
    }),

    todoSaved: (state, payload) => {
      state.items.push(payload); //        👍 correctly typed
      // state.items.push(1);              👍 correctly errors! (1 is not assignable to string)
      //if (state.lengthOfItems > 10) { // ❌ lengthOfItems does not exist on TodoValues, as it's a select(...)ed value
      //}
    },

    lengthOfItems: select(state => {
      return state.items.length; // 👍 correctly typed
    }),
  }
});

Question about select(...) - am I right in thinking that the select()ed value lengthOfItems should appear in the state passed to todoSaved? Or would it only be accessible via getState?

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 20, 2018

Hooks for the above:

function TodoComponent() {
  const num = useStore<Model, number>(state => state.todos.lengthOfItems);    // 👍 correct
  const save = useAction<Model, string>(dispatch => dispatch.todos.saveTodo); // 👍 correct

  // useStore<Model, number>(state => state.todos.items);    
  // 👍 correctly errors - (string[] is not assignable to number)

  //  useAction<Model, number>(dispatch => dispatch.todos.saveTodo);
  // 👍 correctly errors - (payload incompatible - number is not assignable to string)

  return (
    <>
      <p>There are {num} components</p>
      <button type="button" onClick={() => {save("another");}}>Add another!</button>
    </>
  );
}

Not super happy with having to use 2 generics for each of them (both the Model and the payload), but can't see how to get the typings to work without both...

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Nov 20, 2018

Incredibly awesome work. 👍

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 20, 2018

Thanks Sean, got an answer for this?

Question about select(...) - am I right in thinking that the select()ed value lengthOfItems should appear in the state passed to todoSaved? Or would it only be accessible via getState?

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Nov 20, 2018

Ah, missed that question.

Yeah, it would be available anywhere state is accessed (i.e. via actions or getState).

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 20, 2018

Yeah, I thought that would be the case. It means is that there's one minor annoyance with my typings that I'm not sure can be solved (as it ends up being a circular dependency). Users will have to create an additional type for any select(...)s and use it when defining their selects and actions, i.e. instead of just:

interface TodoValues {
  items: Array<string>;
}

interface TodoActions {
  saveTodo: Effect<Model, string>;
  todoSaved: Action<TodoValues, string>;
  lengthOfItems: Select<TodoValues, number>;
}

The following would be needed

interface TodoValues {
  items: Array<string>;
}

interface TodoSelectors {
  lengthOfItems: number;
}

interface TodoActions {
  saveTodo: Effect<Model, string>;
  todoSaved: Action<TodoValues & TodoSelectors, string>;
  lengthOfItems: Select<TodoValues & TodoSelectors, number>;
}

I'll have a crack at solving it though :)

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Nov 20, 2018

I'm no typescript dev, so the below may not be correct syntax but hopefully it communicates an idea.

Would this work:

interface TodoSelectors {
  lengthOfItems: number;
}

interface TodoValues extends TodoSelectors {
  items: Array<string>;
}

interface TodoActions {
  saveTodo: Effect<Model, string>;
  todoSaved: Action<TodoValues, string>;
  lengthOfItems: Select<TodoValues, number>;
}
@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 20, 2018

That would work for TodoActions, but would mean that TodoValues can't then be used to define the model type:

interface Model {
  todos: TodoValues & TodoActions; 
}
// Model.todos.lengthOfItems is now 'number & Select<TodoValues, number>';

I'm going to see if I can get rid of needing a TodoSelectors at all. If it can't be done, so be it, as I don't think it's a dealbreaker.

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 20, 2018

Although, it would work the other way round...

interface TodoValues {
  items: Array<string>;
}

interface TodoValuesAndSelectors extends TodoValues {
  lengthOfItems: number;
}

interface TodoActions {
  saveTodo: Effect<Model, string>;
  todoSaved: Action<TodoValuesAndSelectors, string>;
  lengthOfItems: Select<TodoValuesAndSelectors, number>;
}
@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 20, 2018

One way I might be able to do it is to pass the whole model to Effect and Action, and then tell it which slice of the state tree to operate on:

interface TodoValues {
  items: Array<string>;
}

interface TodoActions {
  saveTodo: Effect<Model, string>;
  todoSaved: Action<Model, 'todos', string>;
  lengthOfItems: Select<Model, 'todos', number>;
}

but not sure if that's worse than the simpler 2-generic version

Update: the idea above doesn't work, as there's the circular dependency of: Model includes Selectors, Selectors are defined by Model

The heartbreaker is that once Model is defined, I have these neat little helpers that give exactly what is needed:

type AllTheValues = ModelValues<Model>; 
// { todos: { items: string[]; lengthOfItems: number } }

type AllTheActions = ModelActions<Model>; 
// { todos: { saveTodo: (s: string) => void; todoSaved: (s: string) => void; } }

type JustTodoValues = ModelValues<Model>['todos'];
// { items: string[]; lengthOfItems: number }

but can't use them to define bits of the Model. Oh well.

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 21, 2018

There are things to be aware of in using our typings with react-redux . mapStateToProps is fine, but mapDispatchToProps will error without casting.

react-redux in connect(...) expects mapDispatchToProps to be a function where dispatch is Redux's Dispatch<Action>. In our case, dispatch has also been decorated with Easy Peasy's actions, which are provided as Dispatch<Model>

Here's mapDispatchToProps without alteration
(i.e. the dispatch parameter is Redux's vanilla Dispatch<Action>)

interface PropsFromState {
  todos: Array<string>;
}

interface PropsFromDispatch {
  addTodo: (todo: string) => void;
}

function EditTodo({ todos, addTodo }: PropsFromState & PropsFromDispatch) {
   ...
}

export default connect<PropsFromState, PropsFromDispatch, {}, ModelValues<Model>>(
  state => ({ todos: state.todos.items }), 
  // 👍 correctly typed

  dispatch => ({ addTodo: dispatch.todos.addTodo }), 
  // ❌ Property 'todos' does not exist on type 'Dispatch<Action>'
)(EditTodo);

and here's mapDispatchToProps as a function that has dispatch as Dispatch<Model> from easy-peasy...

import { Dispatch } from 'easy-peasy';

....

export default connect<PropsFromState, PropsFromDispatch, {}, ModelValues<Model>>(
  state => ({ todos: state.todos.items }), 
  // 👍 correctly typed

  (dispatch: Dispatch<Model>) => ({ addTodo: dispatch.todos.addTodo }), 
  // ❌ Types of parameters 'dispatch' and 'dispatch' are incompatible.
  // ❌ Type 'Dispatch<Action>' is not assignable to type 'Dispatch<Model>'
)(EditTodo);

It can be worked around in one of two ways: either cast dispatch inside mapDispatchToProps:

export default connect<PropsFromState, PropsFromDispatch, {}, ModelValues<Model>>(
  ...

  dispatch => {
    // react-redux thinks dispatch is Redux's Dispatch<Action> but it's actually
    // Easy Peasy's Dispatch<Model>, so cast it
    const easyDispatch = dispatch as Dispatch<Model>;
    return { addTodo: easyDispatch.todos.addTodo };
  }

or cast the mapDispatchToProps function itself

const mapDispatchToProps = (dispatch: Dispatch<Model>) => ({
  addTodo: dispatch.todos.saveTodo,
});

export default connect<PropsFromState, PropsFromDispatch, {}, ModelValues<Model>>(
  ...

  // react-redux expects mapDispatchToProps to receive Redux's Dispatch<Action>,
  // so cast our mapDispatchToProps to a function that receives that
 mapDispatchToProps as (dispatch: ReduxDispatch<ReduxAction>) => PropsFromDispatch,
)(EditTodo);
@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 21, 2018

@aalpgiray - would you be able to try the typings from #33? The more eyes on this the better 🙂

yarn add christianchown/easy-peasy.git#typescript-typings

@aalpgiray

This comment has been minimized.

Copy link

@aalpgiray aalpgiray commented Nov 22, 2018

@christianchown I am on it, but not be able to get it working,
Here is the error I am facing after I install your fork. I'm sure its basic configuration error on my side, but any ideas?

\src\app.tsx:17:29: Cannot resolve dependency 'easy-peasy'

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 22, 2018

I think the build script will need running, as just adding the package won't call prepublish because it didn't get added from npm

cd node_modules/easy-peasy && yarn build should do it

@Gustash

This comment has been minimized.

Copy link

@Gustash Gustash commented Nov 24, 2018

@christianchown maybe submit what you have so far to DefinitelyTyped? Not sure if you've done that already

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Nov 25, 2018

Hi @Gustash - bundling types is usually better, as people won't need to add another dependency, which may get out of sync

@Gustash

This comment has been minimized.

Copy link

@Gustash Gustash commented Nov 25, 2018

Yeah, I agree. Just in case someone is using this until the actual support is there 😄

@rankun203

This comment has been minimized.

Copy link

@rankun203 rankun203 commented Nov 26, 2018

I'm trying out this typings on christianchown/easy-peasy.git#typescript-typings, got stuck at here:

The dispatch var is a Dispatch<Model>, however the action todoSaved reported an error.

The whole codebase of this test was taken from this PR: #33

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Jan 7, 2019

@christianchown do you think it is worth merging this now and then later doing releases to address the 2 issues you highlight in your recent comment?

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Jan 9, 2019

HI @ctrlplusb, the current commit would give false negatives for models like @formula349's above. I would definitely want to add my work for issue no. 2 that addresses that issue before merging anything. Is it okay if I have a few more days to try some things before we go down the route of merging what we have?

Apologies that I've not been more on it, still working through my holiday backlog

@aalpgiray

This comment has been minimized.

Copy link

@aalpgiray aalpgiray commented Jan 9, 2019

I might be able to assist on that one if you have time to explain the situation to me :) or if you already did let me know which comment.

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Jan 9, 2019

Hi @christianchown - no rush at all! Just curious to see if it was possible. Happy to go with your recommendation here.

FYI - I have begun to play around with them myself. Should hopefully have some valuable input shortly.

@skulptur

This comment has been minimized.

Copy link
Contributor

@skulptur skulptur commented Jan 11, 2019

What's the best way for me to start using what you guys have? Is this the last version: https://github.com/christianchown/easy-peasy-typescript/blob/36818ab9947b4f14c0358b1e2f82387015d88021/src/easy-peasy.d.ts ?

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Jan 11, 2019

Hi @skulptur, yes, that's the version I'm basing future work on

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Jan 19, 2019

Hello all, and thanks for bearing with me. I have new definitions that

  • have the improved useAction signature
  • satisfy multi-level Models (well, up to four levels deep, anyway)

The only downside is that I had to change the definition of Dispatch to one that mirrors, rather than employs, Redux's Dispatch. I also had to drop the ability of giving the Dispatch a Redux action, as the new definitions can't infer a Model from an optional type.

@aalpgiray @formula349 @ctrlplusb @skulptur @rankun203 @gdeividas I'd love if you could give these a test if possible? (there's a standalone version here if that's easier to use in testing)

Let me know how you get on!

@skulptur

This comment has been minimized.

Copy link
Contributor

@skulptur skulptur commented Jan 19, 2019

@christianchown thanks so much for your work. I'll start testing these on Monday and let you know if I find anything.

Cheers

@skulptur

This comment has been minimized.

Copy link
Contributor

@skulptur skulptur commented Jan 19, 2019

@christianchown by the way... in my case I'm also having to import React into the definitions. Maybe it's because I have a monorepo setup and the state is its own package. Just letting you know and if it doesn't make a difference then perhaps you could import it in your file?

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Jan 20, 2019

Hi @skulptur, good suggestion: I've now added an import to react in the stand-alone file

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Jan 24, 2019

Thought:

At the moment, for values we have the type ModelValues, and for actions we have ModelActions.

What are your thoughts on dropping the "Model" and just exporting them as Values and Actions?

// (1) do you prefer this
let items = useStore((state: Values<Model>) => state.todos.items);

// (2) or this
let items = useStore((state: ModelValues<Model>) => state.todos.items);

// (3) or have both, as aliases?
@formula349

This comment has been minimized.

Copy link

@formula349 formula349 commented Jan 25, 2019

In my state definition file, I re-export the useStore and useAction with the types already specified.

export function useStore<StateValue>(
    mapState: (state: ModelValues<Store>) => StateValue,
    externals?: Array<any>,
) {
    return useStoreMain<Store, StateValue>(mapState, externals);
}

export function useAction<ActionPayload>(
    mapAction: (dispatch: Dispatch<Store>) => ActionFunction<ActionPayload>,
) {
    return useActionMain(mapAction);
}

So it doesn't matter to me, as I only use that type once. No need to specify types when used in components.

    const x = useStore(s => ({
        permissions: s.app.user.permissions,
    }));
@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Jan 25, 2019

@formula349 - is x in your example correctly typed though? I've been playing with alternative types (#33), and I really hate having to specify the payload type on the useAction hook. Would be great to have the return type inferred.

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Jan 25, 2019

@christianchown

What are your thoughts on dropping the "Model" and just exporting them as Values and Actions?

I definitely prefer Values and Actions. 👍

@formula349

This comment has been minimized.

Copy link

@formula349 formula349 commented Jan 25, 2019

@formula349 - is x in your example correctly typed though? I've been playing with alternative types (#33), and I really hate having to specify the payload type on the useAction hook. Would be great to have the return type inferred.

Yes, x has the properties I've requested properly typed.

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Jan 26, 2019

@formula349 - interesting. I think both of the typings we have in proposals via the PRs require the type of x to be explicitly declared.

I would be interested in seeing your typings, or if you could review #33 / #57 that would be awesome. 👍

@formula349

This comment has been minimized.

Copy link

@formula349 formula349 commented Jan 26, 2019

I'm using the typings from @christianchown.

The problem is useAction/useStore require 2 generic parameters and if you provide 1 generic param, typescript will not infer the second.

I've provided an app-specific export on my state that I use in my components that only requires 1 generic param and hard-codes the Model. This way, typescript is able to infer the second generic based on usage. You can see my export above only has 1 generic param and the Model is my state interface.

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Jan 26, 2019

Great 👍👍

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Jan 26, 2019

Definitely opportunities to merge the best of both PRs into a single awesome solution. 👍

My TypeScript knowledge is still very very new, so I may have skipped on some fundamentals and could have some significant misunderstandings within the PR I created. It will need some bashing.

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Jan 26, 2019

@formula349 - great I got that working in the context of my definitions. Solid tip you gave there too. Makes things far easier having your hooks prebaked like that.

@christianchown

This comment has been minimized.

Copy link
Collaborator Author

@christianchown christianchown commented Jan 27, 2019

Hi all, and great work Sean. I think your typings offer real improvements over mine. Good find with generic recursive type aliases; much better than my level0/level1/level2 shenanigans, with the added advantage of better inference as a result, which removes the need for some of my more inelegant workarounds. I'm happy for you to close #33 in favour of #57

I've run into 2 things with #57, more nice-to-haves than show-stoppers.

  1. An ActionCreator with a void payload is not quite the same as having no payload. Both can be invoked without a value (addTodo() does not error), but a void payload cannot be used where a function has a parameter that will be discarded: <button onClick={addTodo}> will complain that Types of parameters 'payload' and 'event' are incompatible.
  2. This is a problem with Redux's Reducer definition that I've only come across because of its use here. In this commit Redux changed the Reducer signature from
    (state: S, action) => S; to
    (state: S | undefined, action) => S;
    This means that s => s is no longer a correctly typed Reducer for any defined state shape! While I agree that using the dependent definitions is a good thing, I can't see the justification of that particular decision from the Redux team. Oh well.

(A third, personal thing, is that I don't like T or I used as a naming prefix for generics. I think it hurts readability and (as it applies universally) doesn't carry any information. Happy to concede if you like them though)

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Jan 28, 2019

Thanks @christianchown 👍

Top tips too - I am still new to TypeScript so my types are likely to contain some typical newcomer misunderstandings / common pitfalls.

I've take your comments into consideration and have addressed point 1 (ActionCreator) and your note on prefixed generics. Already published. 👍

In regards to 2 (Redux's Reducer signature) - yeah, the signature is definitely annoying in our context as we tend to provide the state upfront via our model. My thinking was to keep it in line with Redux in case someone tried to apply existing typed reducers to easy-peasy. Perhaps it's not really a concern, or perhaps we could create a wrapping type of sorts. Will have a play. 👍

I really appreciate your time and confidence in my implementation.

Super hyped about TypeScript at the moment. This is a huge addition to the library. ❤️

@aalpgiray

This comment has been minimized.

Copy link

@aalpgiray aalpgiray commented Jan 30, 2019

@ctrlplusb do you consider converting your codebase to typescript ?

@ctrlplusb

This comment has been minimized.

Copy link
Owner

@ctrlplusb ctrlplusb commented Jan 30, 2019

Maybe... 😀

@aalpgiray

This comment has been minimized.

Copy link

@aalpgiray aalpgiray commented Jan 30, 2019

Maybe... 😀

Then maybe we can help on that one instead of maintaining typings. :)

ctrlplusb added a commit that referenced this issue Feb 1, 2019
Firstly, I must absolutely stress that none of this work would have been achieved without the monumental effort of @christianchown on his typings work via #33! To be honest given the API of `easy-peasy` I never considered that reasonably sound typings would be at all possible. @christianchown proved me wrong with his work. 

Originally I tried to see if I could solve the issue that was being faced in #33 regarding being bound to a maximum of 4 levels deep of typed state. In doing so however I began to spin out a solution which had a significantly different type definition implementation to that of #33. Eventually it diverged so much that I eventually settled on the idea of spinning out a completely alternative approach rather than trying to change #33 too much.

So I have created this PR instead of trying to comment over #33. I hope that we can take the best of the two to come up with a final solution we are all happy with.

The core approach used in these typing is the use of generic recursive type aliases. I discovered the concept by chance after many hours of googling. The approach appears to work very well for our case, however, I am not sure on how it will perform against large model definitions.

This PR also differs in the manner by which you define your model. Given that I was able to leverage generic recursive type aliases I was able to allow state, actions, selectors, and reducers to be collocated.

Below is a pretty comprehensive example.

```typescript
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import {
  createStore,
  effect,
  reducer,
  select,
  StoreProvider,
  useAction,
  useStore,
  Action,
  Dispatch,
  Effect,
  Reducer,
  Select,
  State,
} from 'easy-peasy'
import { connect } from 'react-redux'

/**
 * Firstly you define your Model
 */

interface TodosModel {
  items: Array<string>
  firstItem: Select<TodosModel, string | void>
  addTodo: Action<TodosModel, string>
}

interface UserModel {
  token?: string
  loggedIn: Action<UserModel, string>
  login: Effect<Model, { username: string; password: string }>
}

interface Model {
  todos: TodosModel
  user: UserModel
  counter: Reducer<number>
}

/**
 * Then you create your store.
 * Note that as we pass the Model into the `createStore` function, so all of our
 * model definition is typed correctly, including inside the actions/helpers etc.
 */

const store = createStore<Model>({
  todos: {
    items: [],
    firstItem: select(state =>
      state.items.length > 0 ? state.items[0] : undefined,
    ),
    addTodo: (state, payload) => {
      state.items.push(payload)
    },
  },
  user: {
    token: undefined,
    loggedIn: (state, payload) => {
      state.token = payload
    },
    login: effect(async (dispatch, payload) => {
      const response = await fetch('/login', {
        method: 'POST',
        body: JSON.stringify(payload),
        headers: {
          'Content-Type': 'application/json',
        },
      })
      const { token } = await response.json()
      dispatch.user.loggedIn(token)
    }),
  },
  counter: reducer((state = 0, action) => {
    switch (action.type) {
      case 'COUNTER_INCREMENT':
        return state + 1
      default:
        return state
    }
  }),
})

/**
 * You can use the "standard" store APIs
 */

console.log(store.getState().todos.firstItem)

store.dispatch({ type: 'COUNTER_INCREMENT' })

store.dispatch.todos.addTodo('Install typescript')

/**
 * You can access state via hooks
 *
 * FYI - see @formula349's tip so that you don't have to constantly provide
 *       types on your useStore and useAction hooks:
 *       #21 (comment)
 */
function MyComponent() {
  const token = useStore((state: State<Model>) => 
    state.user.token
  )

  const login = useAction((dispatch: Dispatch<Model>) => 
	dispatch.user.login,
  )

  return (
    <button onClick={() => login({ username: 'foo', password: 'bar' })}>
      {token || 'Log in'}
    </button>
  )
}

/**
 * Expose the store to your app as normal
 */

ReactDOM.render(
  <StoreProvider store={store}>
    <MyComponent />
  </StoreProvider>,
  document.getElementById('root'),
)

/**
 * We also support typing react-redux
 */

const Counter: React.SFC<{ counter: number }> = ({ counter }) => (
  <div>{counter}</div>
)

connect((state: State<Model>) => ({
  counter: state.counter,
}))(Counter)
```

For anyone interested in testing these you can install:

```
npm install easy-peasy@typescript
```

Things I dislike or need to be considered about these typings:

 - [ ] Having to explicitly add the typing for any injections to any Effect (`Effect<Model, void, Injections>`). It would be great to have an intermediary type that allows us to bake in the value for all effects (`type EffectWithInjections = Effect<*, *, Injections>`) and then be able to declare effects with the new type. 
 - [ ] Unknown performance properties. The recursive type resolution may bite us. We should try testing against a significantly large definition to ensure this isn't a blocker. 

@christianchown - please feel free to merge any bits of this into your PR. I am not preferring one over the other for now, but it may be useful to evolve two forms until we can eventually merge into a single solution that is effective.

Like I said above I still have concerns about the perf implications of the design of these types.
ctrlplusb added a commit that referenced this issue Feb 1, 2019
Firstly, I must absolutely stress that none of this work would have been achieved without the monumental effort of @christianchown on his typings work via #33! To be honest given the API of `easy-peasy` I never considered that reasonably sound typings would be at all possible. @christianchown proved me wrong with his work. 

Originally I tried to see if I could solve the issue that was being faced in #33 regarding being bound to a maximum of 4 levels deep of typed state. In doing so however I began to spin out a solution which had a significantly different type definition implementation to that of #33. Eventually it diverged so much that I eventually settled on the idea of spinning out a completely alternative approach rather than trying to change #33 too much.

So I have created this PR instead of trying to comment over #33. I hope that we can take the best of the two to come up with a final solution we are all happy with.

The core approach used in these typing is the use of generic recursive type aliases. I discovered the concept by chance after many hours of googling. The approach appears to work very well for our case, however, I am not sure on how it will perform against large model definitions.

This PR also differs in the manner by which you define your model. Given that I was able to leverage generic recursive type aliases I was able to allow state, actions, selectors, and reducers to be collocated.

Below is a pretty comprehensive example.

```typescript
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import {
  createStore,
  effect,
  reducer,
  select,
  StoreProvider,
  useAction,
  useStore,
  Action,
  Dispatch,
  Effect,
  Reducer,
  Select,
  State,
} from 'easy-peasy'
import { connect } from 'react-redux'

/**
 * Firstly you define your Model
 */

interface TodosModel {
  items: Array<string>
  firstItem: Select<TodosModel, string | void>
  addTodo: Action<TodosModel, string>
}

interface UserModel {
  token?: string
  loggedIn: Action<UserModel, string>
  login: Effect<Model, { username: string; password: string }>
}

interface Model {
  todos: TodosModel
  user: UserModel
  counter: Reducer<number>
}

/**
 * Then you create your store.
 * Note that as we pass the Model into the `createStore` function, so all of our
 * model definition is typed correctly, including inside the actions/helpers etc.
 */

const store = createStore<Model>({
  todos: {
    items: [],
    firstItem: select(state =>
      state.items.length > 0 ? state.items[0] : undefined,
    ),
    addTodo: (state, payload) => {
      state.items.push(payload)
    },
  },
  user: {
    token: undefined,
    loggedIn: (state, payload) => {
      state.token = payload
    },
    login: effect(async (dispatch, payload) => {
      const response = await fetch('/login', {
        method: 'POST',
        body: JSON.stringify(payload),
        headers: {
          'Content-Type': 'application/json',
        },
      })
      const { token } = await response.json()
      dispatch.user.loggedIn(token)
    }),
  },
  counter: reducer((state = 0, action) => {
    switch (action.type) {
      case 'COUNTER_INCREMENT':
        return state + 1
      default:
        return state
    }
  }),
})

/**
 * You can use the "standard" store APIs
 */

console.log(store.getState().todos.firstItem)

store.dispatch({ type: 'COUNTER_INCREMENT' })

store.dispatch.todos.addTodo('Install typescript')

/**
 * You can access state via hooks
 *
 * FYI - see @formula349's tip so that you don't have to constantly provide
 *       types on your useStore and useAction hooks:
 *       #21 (comment)
 */
function MyComponent() {
  const token = useStore((state: State<Model>) => 
    state.user.token
  )

  const login = useAction((dispatch: Dispatch<Model>) => 
	dispatch.user.login,
  )

  return (
    <button onClick={() => login({ username: 'foo', password: 'bar' })}>
      {token || 'Log in'}
    </button>
  )
}

/**
 * Expose the store to your app as normal
 */

ReactDOM.render(
  <StoreProvider store={store}>
    <MyComponent />
  </StoreProvider>,
  document.getElementById('root'),
)

/**
 * We also support typing react-redux
 */

const Counter: React.SFC<{ counter: number }> = ({ counter }) => (
  <div>{counter}</div>
)

connect((state: State<Model>) => ({
  counter: state.counter,
}))(Counter)
```

For anyone interested in testing these you can install:

```
npm install easy-peasy@typescript
```

Things I dislike or need to be considered about these typings:

 - [ ] Having to explicitly add the typing for any injections to any Effect (`Effect<Model, void, Injections>`). It would be great to have an intermediary type that allows us to bake in the value for all effects (`type EffectWithInjections = Effect<*, *, Injections>`) and then be able to declare effects with the new type. 
 - [ ] Unknown performance properties. The recursive type resolution may bite us. We should try testing against a significantly large definition to ensure this isn't a blocker. 

@christianchown - please feel free to merge any bits of this into your PR. I am not preferring one over the other for now, but it may be useful to evolve two forms until we can eventually merge into a single solution that is effective.

Like I said above I still have concerns about the perf implications of the design of these types.
@ctrlplusb ctrlplusb closed this Feb 1, 2019
ctrlplusb added a commit that referenced this issue Feb 6, 2019
Firstly, I must absolutely stress that none of this work would have been achieved without the monumental effort of @christianchown on his typings work via #33! To be honest given the API of `easy-peasy` I never considered that reasonably sound typings would be at all possible. @christianchown proved me wrong with his work. 

Originally I tried to see if I could solve the issue that was being faced in #33 regarding being bound to a maximum of 4 levels deep of typed state. In doing so however I began to spin out a solution which had a significantly different type definition implementation to that of #33. Eventually it diverged so much that I eventually settled on the idea of spinning out a completely alternative approach rather than trying to change #33 too much.

So I have created this PR instead of trying to comment over #33. I hope that we can take the best of the two to come up with a final solution we are all happy with.

The core approach used in these typing is the use of generic recursive type aliases. I discovered the concept by chance after many hours of googling. The approach appears to work very well for our case, however, I am not sure on how it will perform against large model definitions.

This PR also differs in the manner by which you define your model. Given that I was able to leverage generic recursive type aliases I was able to allow state, actions, selectors, and reducers to be collocated.

Below is a pretty comprehensive example.

```typescript
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import {
  createStore,
  effect,
  reducer,
  select,
  StoreProvider,
  useAction,
  useStore,
  Action,
  Dispatch,
  Effect,
  Reducer,
  Select,
  State,
} from 'easy-peasy'
import { connect } from 'react-redux'

/**
 * Firstly you define your Model
 */

interface TodosModel {
  items: Array<string>
  firstItem: Select<TodosModel, string | void>
  addTodo: Action<TodosModel, string>
}

interface UserModel {
  token?: string
  loggedIn: Action<UserModel, string>
  login: Effect<Model, { username: string; password: string }>
}

interface Model {
  todos: TodosModel
  user: UserModel
  counter: Reducer<number>
}

/**
 * Then you create your store.
 * Note that as we pass the Model into the `createStore` function, so all of our
 * model definition is typed correctly, including inside the actions/helpers etc.
 */

const store = createStore<Model>({
  todos: {
    items: [],
    firstItem: select(state =>
      state.items.length > 0 ? state.items[0] : undefined,
    ),
    addTodo: (state, payload) => {
      state.items.push(payload)
    },
  },
  user: {
    token: undefined,
    loggedIn: (state, payload) => {
      state.token = payload
    },
    login: effect(async (dispatch, payload) => {
      const response = await fetch('/login', {
        method: 'POST',
        body: JSON.stringify(payload),
        headers: {
          'Content-Type': 'application/json',
        },
      })
      const { token } = await response.json()
      dispatch.user.loggedIn(token)
    }),
  },
  counter: reducer((state = 0, action) => {
    switch (action.type) {
      case 'COUNTER_INCREMENT':
        return state + 1
      default:
        return state
    }
  }),
})

/**
 * You can use the "standard" store APIs
 */

console.log(store.getState().todos.firstItem)

store.dispatch({ type: 'COUNTER_INCREMENT' })

store.dispatch.todos.addTodo('Install typescript')

/**
 * You can access state via hooks
 *
 * FYI - see @formula349's tip so that you don't have to constantly provide
 *       types on your useStore and useAction hooks:
 *       #21 (comment)
 */
function MyComponent() {
  const token = useStore((state: State<Model>) => 
    state.user.token
  )

  const login = useAction((dispatch: Dispatch<Model>) => 
	dispatch.user.login,
  )

  return (
    <button onClick={() => login({ username: 'foo', password: 'bar' })}>
      {token || 'Log in'}
    </button>
  )
}

/**
 * Expose the store to your app as normal
 */

ReactDOM.render(
  <StoreProvider store={store}>
    <MyComponent />
  </StoreProvider>,
  document.getElementById('root'),
)

/**
 * We also support typing react-redux
 */

const Counter: React.SFC<{ counter: number }> = ({ counter }) => (
  <div>{counter}</div>
)

connect((state: State<Model>) => ({
  counter: state.counter,
}))(Counter)
```

For anyone interested in testing these you can install:

```
npm install easy-peasy@typescript
```

Things I dislike or need to be considered about these typings:

 - [ ] Having to explicitly add the typing for any injections to any Effect (`Effect<Model, void, Injections>`). It would be great to have an intermediary type that allows us to bake in the value for all effects (`type EffectWithInjections = Effect<*, *, Injections>`) and then be able to declare effects with the new type. 
 - [ ] Unknown performance properties. The recursive type resolution may bite us. We should try testing against a significantly large definition to ensure this isn't a blocker. 

@christianchown - please feel free to merge any bits of this into your PR. I am not preferring one over the other for now, but it may be useful to evolve two forms until we can eventually merge into a single solution that is effective.

Like I said above I still have concerns about the perf implications of the design of these types.
ctrlplusb added a commit that referenced this issue Feb 11, 2019
Firstly, I must absolutely stress that none of this work would have been achieved without the monumental effort of @christianchown on his typings work via #33! To be honest given the API of `easy-peasy` I never considered that reasonably sound typings would be at all possible. @christianchown proved me wrong with his work. 

Originally I tried to see if I could solve the issue that was being faced in #33 regarding being bound to a maximum of 4 levels deep of typed state. In doing so however I began to spin out a solution which had a significantly different type definition implementation to that of #33. Eventually it diverged so much that I eventually settled on the idea of spinning out a completely alternative approach rather than trying to change #33 too much.

So I have created this PR instead of trying to comment over #33. I hope that we can take the best of the two to come up with a final solution we are all happy with.

The core approach used in these typing is the use of generic recursive type aliases. I discovered the concept by chance after many hours of googling. The approach appears to work very well for our case, however, I am not sure on how it will perform against large model definitions.

This PR also differs in the manner by which you define your model. Given that I was able to leverage generic recursive type aliases I was able to allow state, actions, selectors, and reducers to be collocated.

Below is a pretty comprehensive example.

```typescript
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import {
  createStore,
  effect,
  reducer,
  select,
  StoreProvider,
  useAction,
  useStore,
  Action,
  Dispatch,
  Effect,
  Reducer,
  Select,
  State,
} from 'easy-peasy'
import { connect } from 'react-redux'

/**
 * Firstly you define your Model
 */

interface TodosModel {
  items: Array<string>
  firstItem: Select<TodosModel, string | void>
  addTodo: Action<TodosModel, string>
}

interface UserModel {
  token?: string
  loggedIn: Action<UserModel, string>
  login: Effect<Model, { username: string; password: string }>
}

interface Model {
  todos: TodosModel
  user: UserModel
  counter: Reducer<number>
}

/**
 * Then you create your store.
 * Note that as we pass the Model into the `createStore` function, so all of our
 * model definition is typed correctly, including inside the actions/helpers etc.
 */

const store = createStore<Model>({
  todos: {
    items: [],
    firstItem: select(state =>
      state.items.length > 0 ? state.items[0] : undefined,
    ),
    addTodo: (state, payload) => {
      state.items.push(payload)
    },
  },
  user: {
    token: undefined,
    loggedIn: (state, payload) => {
      state.token = payload
    },
    login: effect(async (dispatch, payload) => {
      const response = await fetch('/login', {
        method: 'POST',
        body: JSON.stringify(payload),
        headers: {
          'Content-Type': 'application/json',
        },
      })
      const { token } = await response.json()
      dispatch.user.loggedIn(token)
    }),
  },
  counter: reducer((state = 0, action) => {
    switch (action.type) {
      case 'COUNTER_INCREMENT':
        return state + 1
      default:
        return state
    }
  }),
})

/**
 * You can use the "standard" store APIs
 */

console.log(store.getState().todos.firstItem)

store.dispatch({ type: 'COUNTER_INCREMENT' })

store.dispatch.todos.addTodo('Install typescript')

/**
 * You can access state via hooks
 *
 * FYI - see @formula349's tip so that you don't have to constantly provide
 *       types on your useStore and useAction hooks:
 *       #21 (comment)
 */
function MyComponent() {
  const token = useStore((state: State<Model>) => 
    state.user.token
  )

  const login = useAction((dispatch: Dispatch<Model>) => 
	dispatch.user.login,
  )

  return (
    <button onClick={() => login({ username: 'foo', password: 'bar' })}>
      {token || 'Log in'}
    </button>
  )
}

/**
 * Expose the store to your app as normal
 */

ReactDOM.render(
  <StoreProvider store={store}>
    <MyComponent />
  </StoreProvider>,
  document.getElementById('root'),
)

/**
 * We also support typing react-redux
 */

const Counter: React.SFC<{ counter: number }> = ({ counter }) => (
  <div>{counter}</div>
)

connect((state: State<Model>) => ({
  counter: state.counter,
}))(Counter)
```

For anyone interested in testing these you can install:

```
npm install easy-peasy@typescript
```

Things I dislike or need to be considered about these typings:

 - [ ] Having to explicitly add the typing for any injections to any Effect (`Effect<Model, void, Injections>`). It would be great to have an intermediary type that allows us to bake in the value for all effects (`type EffectWithInjections = Effect<*, *, Injections>`) and then be able to declare effects with the new type. 
 - [ ] Unknown performance properties. The recursive type resolution may bite us. We should try testing against a significantly large definition to ensure this isn't a blocker. 

@christianchown - please feel free to merge any bits of this into your PR. I am not preferring one over the other for now, but it may be useful to evolve two forms until we can eventually merge into a single solution that is effective.

Like I said above I still have concerns about the perf implications of the design of these types.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
9 participants
You can’t perform that action at this time.