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....
Would certainly be awesome!
No rush at all. Let's keep this issue open and perhaps we can get some more volunteers to help. π
FYI, I've been really stoked on all of your contributions and have sent you an invite to be a collaborator on this project. π
Tried and failed. :) Have anyone started yet?
@aalpgiray - I've started, not there yet though
Great idea, let us know how can we help!
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
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...
@christianchown. Does definitely typed provide a sufficient guide ?
Phew! @christianchown you look to be a Typescript god. That is some impressive typing going on. Excited about this. π
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?
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...
Incredibly awesome work. π
Thanks Sean, got an answer for this?
Question about
select(...)- am I right in thinking that the select()ed valuelengthOfItemsshould appear in the state passed totodoSaved? Or would it only be accessible viagetState?
Ah, missed that question.
Yeah, it would be available anywhere state is accessed (i.e. via actions or getState).
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 :)
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>;
}
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.
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>;
}
~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.
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);
@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
@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'
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
@christianchown maybe submit what you have so far to DefinitelyTyped? Not sure if you've done that already
Hi @Gustash - bundling types is usually better, as people won't need to add another dependency, which may get out of sync
Yeah, I agree. Just in case someone is using this until the actual support is there π
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: https://github.com/ctrlplusb/easy-peasy/pull/33
I think there is something wrong with FunctionsWithoutFirstParamor something around there may be IsMoreThanOneParam because it removes not only first param but all.
changing this bit
type FunctionWithoutFirstParam<F> = IsMoreThanOneParam<F> extends () => void
? (payload: Param1<F>) => void
: () => void;
with this one
type FunctionWithoutFirstParam<F> =
(payload: Param1<F>) => void;
solves problem in this particular use, but of course breakes the intedted typing.
Hi @rankun203 and @aalpgiray - thanks for testing (and sorry I didn't get back sooner)
@rankun203 I'm not seeing that error? I am using https://github.com/christianchown/easy-peasy/commit/6db9d0067767739d3ac2e75c4b45aef53f3a7351, Typescript 3.1.3, and the code at https://github.com/ctrlplusb/easy-peasy/pull/33#issue-232507824 saved as pr.tsx gives me correct typing, as below:

If you still get a problem, are you able to share a gist/minimal repo?
@aalpgiray the idea behind FunctionWithoutFirstParam is to take a function type, and return a function type with the first parameter removed, so that
todoSaved(state, payload) in the Model gets exposed as todoSaved(payload) in the dispatch (which it would in yours) π, however it also needs to do:
clearTodos(state) in the Model gets exposed as clearTodos() in the dispatch (which in yours would be clearTodos(payload: {}) β
So it looks like IsMoreThanOneParam isn't working as specified. Do you see the same problem as @rankun203? If you do, can you share a repo? (or a file along with the commit hash of easy-peasy, typescript version & tsconfig)?
yes it was the same problem. here is the repo
https://github.com/aalpgiray/easy-peasy-demo
I Updated Typescript to latest "typescript": "^3.3.0-dev.20181130" now its worse

Thanks for the repo. I'll have a look.
Here's a repo that works: https://github.com/christianchown/easy-peasy-typescript
I wonder what's the difference?
Aha - first error is that I should have made type-zoo a dependency, not a devDependency in easy-peasy
yarn add type-zoo to your repo, and you get rid of the anys - but it still thinks todoSaved has no parameters...
it was the initial problem, somehow IsMoreThanOneParam is not working correctly but I did not understand how it does what it does. If you point me towards the information about that as well I'll be more than happy to look into.
this you your repo after I have ran yarn


I have pushed a new commit to my fork
Try this with yours
yarn remove easy-peasy
yarn add christianchown/easy-peasy.git#0a09daca
I think there may be a caching issue with yarn. I added a third generic to Effect a few days ago - https://github.com/christianchown/easy-peasy/commit/935582caa4889a5d589a8c9fa0ff7e25eb85b2c3 - so that it can return values
Adding my fork from a specific commit is probably wiser while I'm still developing
(ps thank you so much for helping btw!)
IsMoreThanOneParam<F> returns
F, if F is a function with more than one parameternever otherwiseso FunctionWithoutFirstParam<T> can then use it to conditionally yield
(payload: P) => void , or () => void depending on whether T is a function with
(firstArg: any, payload: P) or just (firstArg: any) => voidMy mistake was here - https://github.com/christianchown/easy-peasy/commit/0a09dacae996853f290528f9f1ede509bab3016c - FunctionWithoutFirstParam<T> thought that any function F would extends () => void. I changed it to extends Function and it works better
Yes, it resolves the problem. Thank you again. Be back with more feedback.
Thanks for the great work on this!
I'm having trouble with dispatch on an effect. TypeScript is giving me this error:
[ts] Cannot invoke an expression whose type lacks a call signature.
Additionally, the intellisense on the dispatch object is showing all properties on the Model, not just the functions. I've traced it down to a few edits I've made locally to get this working for me. Not sure why no-one else is seeing this.
I changed the ModelActions

to:

dispatch functions were showing up along with the merged Redux.Dispatch type, but the actions with no payload were showing up as (payload: void). I changed your FunctionWithoutFirstParam type to extend never instead of Function (reversing the logic) and it seems to be working great.

Let me know what you think.
Hi @formula349, are you able to share the Model that's giving you difficulties?
Either that, or can you clone https://github.com/christianchown/easy-peasy-typescript/ and try your Model in that repo?
No problem. I've been running this locally with your latest d.ts file, so I don't think cloning your repo will help. I had to wrap your code in declare module 'easy-peasy' {} so it would work with the easy-peasy NPM package as-is.
interface User {
id: number
name: string
email: string
permissions: {
GlobalAdmin?: boolean
Admin?: boolean
[k: string]: boolean | undefined
}
}
interface Model {
ready: boolean
logoBaseUrl: string,
user: User
clients: ClientType[]
selectedClient: {
id: number
sites: SiteType[]
}
isAuthenticated: Select<{ user: { id: number } }, boolean>
// Actions
login: Effect<Model, LoginCredentials>
logout: Action<Model, void>
// Events
_handleLogin: Action<Model, LoginResponse>
}
The store creation...
export const Store = createStore<Model>({
ready: false,
logoBaseUrl: '',
user: {
id: 0,
name: '',
email: '',
permissions: {},
},
clients: [],
selectedClient: {
id: 0,
sites: [],
},
isAuthenticated: select(s => !!s.user.id),
login: effect(async (dispatch, credentials) => {
const results = await login(credentials);
dispatch._handleLogin(results);
}),
logout: (state) => {
clearLoginData();
return {
...state,
clients: [],
selectedClient: { id: 0, sites: [] },
user: { id: 0, name: '', email: '', permissions: {} },
}
},
_handleLogin: (state: Model, p: LoginResponse) => {
if(p.isError) {
} else {
// Update state
state.user.email = p.userName;
state.user.id = Number(p.id);
state.user.name = p.name;
}
},
})
You have this piece here...
type IsMoreThanOneParam<Func> = Func extends (a: any, b: undefined, ...args: Array<any>) => any ? Func : never;
type FunctionWithoutFirstParam<F> = IsMoreThanOneParam<F> extends Function
? (payload: Param1<F>) => void
: () => void;
I switched mine to extends never instead of extends Function in order to reverse the logic on the ternary operator. The real problem it solves is the returned type for IsMoreThanOneParam. Take a look at the results of these tests.
const f0 = () => { }
const f1 = (a: string) => { }
const f2 = (a: string, b: number) => { }
type t0 = IsMoreThanOneParam<typeof f0> // t0 = () => void
type t1 = IsMoreThanOneParam<typeof f1> // t1 = (a: string) => void
type t2 = IsMoreThanOneParam<typeof f2> // t2 = never
Now lets change it to:
type IsMoreThanOneParam<Func> = Func extends (a: any, b: undefined, ...args: Array<any>) => any ? false : true;
type FunctionWithoutFirstParam<F> = IsMoreThanOneParam<F> extends true
? (payload: Param1<F>) => void
: () => void;
And then run those tests
const f0 = () => { }
const f1 = (a: string) => { }
const f2 = (a: string, b: number) => { }
// types are easier to understand here
type t0 = IsMoreThanOneParam<typeof f0> // t0 = false
type t1 = IsMoreThanOneParam<typeof f1> // t1 = false
type t2 = IsMoreThanOneParam<typeof f2> // t2 = true
// types look like what I'd expect now
type f0 = FunctionWithoutFirstParam<typeof f0> // f0 = () => void
type f1 = FunctionWithoutFirstParam<typeof f1> // f1 = () => void
type f2 = FunctionWithoutFirstParam<typeof f2> // f2 = (payload: number) => void
Hi @formula349, I think I see what's going on. What you've called a Model is actually a slice of state.
I would expect not this:
export const Store = createStore<Model>({
ready: false,
logoBaseUrl: "",
...
isAuthenticated: select(s => !!s.user.id),
login: effect(async (dispatch, credentials) => {
const results = await login(credentials);
dispatch._handleLogin(results);
}),
but this
export const Store = createStore<Model>({
user: {
ready: false,
logoBaseUrl: "",
...
isAuthenticated: select(s => !!s.user.id),
login: effect(async (dispatch, credentials) => {
}
});
I've moved the actions around a few times as I've worked on modeling what my state needs to look like. The reason for potentially putting the actions in the root of the Model is so they have access to other slices besides the user slice.
Either way, the issues remain. I've worked through them with the edits I described above. Any thoughts on this post? I was just hoping to help work through the issues I've seen and get a solid set of types in this project.
I'm working on some other issues now that I will share once I figure it out having to do with useAction.
Thanks again for your help.
Hi @formula349, I think I see what's going on. What you've called a
Modelis actually a slice of state.
I see now why you're suggesting adding the actions to the slice. The typings work correctly in that instance, but not when the action is on the root store.
I'm finding this library has too much 'magic' for typescript to properly handle. Finding it difficult to refactor the slices to their own file without having to add a lot of extra typing that isn't necessary when defined in-line.
Yes, I'd assumed each slice would always be under a key. I will have to see if there's a way to glean the ModelValues and ModelActions if they're attached to the root of the model.
@christianchown
how should I dispatch an action for the counter in this instance? I use any for dispatch and this works. But I could not figure out how this should be typed. Or should we still use this kind of stuff anyway? Is this defies the real use case of easy-peasy? @ctrlplusb

here is the repo. The code is in Counter.tsx
@aalpgiray - nice catch! useAction's first parameter should be Dispatch<Model>, not ModelActions<Model>. I've added a commit to fix this.
Swap out https://github.com/christianchown/easy-peasy/commit/0a09dacae996853f290528f9f1ede509bab3016c for https://github.com/christianchown/easy-peasy/commit/7f1aad3b884f45219c1d301b537d3ae33a9d2bec, and dispatch will be correctly typed inside Counter.tsx
_Edit_, also you can provide a second generic here:
const count = useAction<ICounterActions, void>(...);
This second generic will type the return value from useAction, and can either be a payload (or _undefined_ or _void_ for no payload, like your count), or the function type itself.
The same applies for useStore:
const counter = useStore<IModel, number>(....)
will both type counter, and ensure the callback returns the right type of value
I'm finding this library has too much 'magic' for typescript to properly handle. Finding it difficult to refactor the slices to their own file without having to add a lot of extra typing that isn't necessary when defined in-line.
@formula349 you may be right. Thanks to your example, I can see that it's not just the root level that can have actions, effects, selects and reducers, it's _any point_ in the model. This complicates things, to say the least. I've not given up yet though.
I think this
type FunctionWithoutFirstParam<F> = IsMoreThanOneParam<F> extends Function
? (payload: Param1<F>) => void
: () => void;
should be like this
type FunctionWithoutFirstParam<F> = IsMoreThanOneParam<F> extends Function
? () => void
: (payload: Param1<F>) => void;
referring to this error

Edit: No it should not be like that. π :) but how
Not sure where counter.increment comes from? In your _easy-peasy-hooks_ repo I have, counter is a reducer(...)
Sorry I changed It and carelessly push it to the master. I was a basic parameterless action in the model
{
...
increment: (state) => { ... },
...
}
I have a new (working!) IsMoreThanOneParam which corrects the logic in FunctionWithoutFirstParam. Give https://github.com/christianchown/easy-peasy/commit/153a146d6d44753445ed1f85247b21ed3d53f9d0 a try
Hi @formula349, I have a new set of typings that work with your Model!
They are here: https://github.com/christianchown/easy-peasy-typescript/blob/formula-wip/src/easy-peasy.d.ts
What I have had to do is form ModelActions and ModelValues level-by-level (like this), so it isn't pretty! I also only did the first 4 levels so if you have a model like:
dispatch.level0.level1.level2.level3.anEffect(); it'll be typesafe, but will break down at .level4 onwards. I could do further levels, but it could get silly...
One thing to note is that your version had Actions genericised as Action<Model>, but they need to be Action<SliceOfModel>
However, my bastardised version of the Model you gave me: https://github.com/christianchown/easy-peasy-typescript/blob/formula-wip/src/formula.ts is type safe!
@aalpgiray are you able to also try https://github.com/christianchown/easy-peasy-typescript/blob/f5f8ac23a1016fcc41732d16e6b797f71af0d212/src/easy-peasy.d.ts (with just the normal easy-peasy rather than my fork), and let me know if they work?
(You'll need type-zoo and redux as dependencies)
@christianchown I've made some significant changes to my model as I've adapted for the way easy-peasy works. One of which is to only have 1 level of slices, and all functions are within those slices.
I switched out my typings for your updated typings.
All of my state properties that are typed as an array of items are not working properly when retrieved in useStore.
export interface Slice {
clients: IClient[]
}
export const slice = {
clients: [],
}
const state = useStore(s => ({
clients: s.slice.clients,
});

Hi @christianchown I was working on useAction/Store with less generic parameters
export function useAction<IModel, ActionPayload>(
mapAction: <DispatchModel extends Dispatch<IModel>>(dispatch: DispatchModel) => ActionPayload,
): ActionPayload;
export function useStore<Model, StateValue>(
mapState: <ModelState extends ModelValues<Model>>(state: ModelState) => StateValue,
externals?: Array<any>,
): StateValue;
md5-d30fa231cadb5c310fda5f538cb56d81
const todos = useStore((state: ModelValues
```
Here is the fork. https://github.com/aalpgiray/easy-peasy/tree/typescript-typings
@formula349 ....
@christianchown I've made some significant changes to my model as I've adapted for the way easy-peasy works. One of which is to only have 1 level of slices, and all functions are within those slices.
Cool, but until I saw your Model, I didn't realise Easy Peasy allowed multiple levels where actions, effects, selects and reducers could be at any level. I had to prove to myself that you were right - and I can see benefits in allowing them at the root level, or deeper than level 1 in the tree.
All of my state properties that are typed as an array of items are not working properly when retrieved in
useStore.
Dumb error on my part! I forgot to treat arrays like atomic values, and my typings assumed they were objects to be recursed into...
@aalpgiray
Nice work!
In https://github.com/aalpgiray/easy-peasy/commit/3d5113465f2d424e46808ffb0e389fdb769e7278, I like restricting dispatch to being Redux's action if you provide an action generic; however, because Easy Peasy actually decorates store.dispatch with the Model actions, meaning the object is a merge of dispatch({type: ACTION_TYPE}) and dispatch.state.action(), so it does need to keep the union. I think something like
type Dispatch<Model = any> =
Model extends Redux.Action
? Redux.Dispatch<Model>
: Redux.Dispatch & ModelActions<Model>;
will be more correct.
I definitely like the pattern behind https://github.com/aalpgiray/easy-peasy/commit/f4e45acc2b7410232498e2377688f19aad64531f as the terser phrasing is much neater. Will definitely have a play and incorporate it :)
@christianchown @aalpgiray @formula349 @Gustash @m5r - I really appreciate all the effort you are putting into ironing out the type definitions.
My Typescript learning journey is going extremely well and I am super excited to be able to use these type definitions. I may look to release another medium article describing how to use Easy Peasy with types after we merge to master. Exciting times.
@christianchown everything is looking good so far! I'll let you know if I run into any issues as I progress. Thanks again!
Hi @aalpgiray, for the life of me I cannot get useAction to behave the way I'd like to. If I use your ternary for Dispatch, then you can't use it as a standard Redux dispatch without casting:
type Dispatch<Model> = Model extends Redux.Action
? Redux.Dispatch<Model>
: ModelActions<Model>;
let store = createStore<Model>(...);
const save = useAction((dispatch: Dispatch<Model>) => dispatch.todos.saveTodo); // works!
store.dispatch({type: 'SOME_ACTION'}); // errors
Using my old formulation errors for useAction, because TS can no longer infer Model.
type Dispatch<Model> = ModelActions<Model> & Redux.Dispatch;
let store = createStore<Model>(...);
const save = useAction((dispatch: Dispatch<Model>) => dispatch.todos.saveTodo); // errors
store.dispatch({type: 'SOME_ACTION'}); // works!
The only phrasing I have found that works is not to use Redux.Dispatch, but a simulacrum of it
type ReduxDispatch = (action: Redux.Action) => Redux.Action;
type Dispatch<Model> = ModelActions<Model> & ReduxDispatch;
let store = createStore<Model>(...);
const save = useAction((dispatch: Dispatch<Model>) => dispatch.todos.saveTodo); // works!
store.dispatch({type: 'SOME_ACTION'}); // works!
which may be what I end up having to do, because your useAction phrasing is so superior.
const count = () =>
store.dispatch<CounterActions>({
type: "INCREMENT",
});
or
const count = () =>
store.dispatch({
type: "INCREMENT",
});
These are working with mine. How is your final typing? Here is the sample. You may want to link redux-first-history with this repo. Because typings pull requests has not been merged by the time you pull.
And here is the easy-peasy fork that I changed the typings.
Sorry, I was not looking after this for a while and now I could not remember the final state of things.
Ah I see. Those work fine with the existing formulations of ModelActions and ModelValues. Unfortunately, I made substantial changes here: https://github.com/christianchown/easy-peasy-typescript/blob/36818ab9947b4f14c0358b1e2f82387015d88021/src/easy-peasy.d.ts, to allow for actions, reducers, selects and effects to be placed at any level of the Model tree... and it stops that version of dispatch working with useAction. Gah!
I'll see what I can do
To see why I needed that commit above, I've added some extra types to easy-peasy-hooks: https://github.com/christianchown/easy-peasy-hooks/commit/dc4f143e77bb73756fac83b964ad6bd12d746767
Which should allow this:
const store = createStore<IModel>(
{
router: reducer(routerReducer),
todos,
counter
},
{
middleware: [routerMiddleware]
}
);
store.dispatch({ type: "OK" }); // works!
store.dispatch.todos.clearDoneTodos(); // works!
store.dispatch.todos.deep.incrementDeepValue(); // does not work
store.dispatch.todos.deep.deeper.incrementDeeperValue(); // does not work
My commit that does the Level0Actions & Level1Actions & Level2Actions etc. stuff allows for todos.deep and todos.deep.deeper, but breaks the other things, unless I change dispatch like here
Hi, @christianchown do you need help on anything in particular. I was not following the deeply nested state issue from the beginning. What is the current issue?
@christianchown can you check this
@ctrlplusb and @christianchown bit hard to understand what is the actual status on this? can we merge this ?
hello @aalpgiray and @gdeividas - apologies, been away for Xmas
current status is that the PR needs two things before it can be merged
useAction needs adjusting to match the superior signature from @aalpgiray ModelActions and ModelValues need to work for multi-level models@aalpgiray has a commit that works for 1
I have a commit that works for 2
but haven't yet been able to get them to play nice together. I'll be back on it properly in the next few days - I haven't checked https://github.com/ctrlplusb/easy-peasy/issues/21#issuecomment-449603138. We'll get there soon hopefully!
@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?
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
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.
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.
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 ?
Hi @skulptur, yes, that's the version I'm basing future work on
Hello all, and thanks for bearing with me. I have new definitions that
useAction signatureThe 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!
@christianchown thanks so much for your work. I'll start testing these on Monday and let you know if I find anything.
Cheers
@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?
Hi @skulptur, good suggestion: I've now added an import to react in the stand-alone file
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?
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,
}));
@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.
@christianchown
What are your thoughts on dropping the "Model" and just exporting them as Values and Actions?
I definitely prefer Values and Actions. π
@formula349 - is
xin 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.
@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. π
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.
Great ππ
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.
@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.
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.
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._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;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)
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. β€οΈ
@ctrlplusb do you consider converting your codebase to typescript ?
Maybe... π
Maybe... π
Then maybe we can help on that one instead of maintaining typings. :)
Most helpful comment
IsMoreThanOneParam<F>returnsF, ifFis a function with more than one parameterneverotherwiseso
FunctionWithoutFirstParam<T>can then use it to conditionally yield(payload: P) => void, or() => voiddepending on whether
Tis a function with(firstArg: any, payload: P)or just(firstArg: any) => voidMy mistake was here - https://github.com/christianchown/easy-peasy/commit/0a09dacae996853f290528f9f1ede509bab3016c -
FunctionWithoutFirstParam<T>thought that any functionFwouldextends () => void. I changed it toextends Functionand it works better