-
-
Notifications
You must be signed in to change notification settings - Fork 98
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 createReducer - a typesafe reducer factory using object map (including getType() support) #106
Comments
Ok good small-news: I fix the error on the |
Hey, I suppose this issue is related right? piotrwitek/react-redux-typescript-guide#121 So you want to include I implemented it but there is an issue with computed properties assertions which I believe is a bug in TypeScript. Maybe you can investigate it further in TypeScript repo issue as I want to focus on clearing out the technical debt before adding new features. Implemented here: piotrwitek/react-redux-typescript-guide@6502fd4#diff-8ed224eb6b7ff91fd900f963dc88b328 |
Ah true! I didn't see this issue in your other repository. That's indeed the idea for the moment (I have another suggestion to improve Ok, I don't know if you already check my repository, but we have more or less the same problem. If I use the full path string constant as a |
Yeah I agree, from the example above I think the problem is not in the |
Yes, I'm trying to figure out a small test example for them 👍 |
Ah! nice 🙂 I will post it right now in there repository 👍 |
Thanks a lot @MasGaNo! Please link to this issue so we can track it here. |
Here we go: microsoft/TypeScript#29718 |
Blocked by TypeScript bug, we'll get back to this idea when it's fixed upstream. |
Well, I hope it will be really fixed for TypeScript 3.4.0, as they are currently already working on the 3.3.3 (btw, almost complete). Wait&see. For the moment, there is still the workaround with constant, so it remains a good progress against the big |
a convenient example from
|
Hi @rifler But personally, I prefer to keep the ObjectMap approach because I like the idea to have my IDE tells me what are the non-implemented reducer, and help me to avoid duplicate redefinition. Here I don't know if I overwrite the previous implementation or if it will be extended. At least with this kind of implementation could be more useful IMHO: type ReducerRegisterGlob<T> = {handlerRegister: ReducerRegister<T>};
type ReducerRegister<T> = <P extends T>(value: P, callback: Function) => ReducerRegisterGlob<Exclude<T, P>>;
function createReducers<T>(callback: (handlerRegister: ReducerRegister<T>) => void) {
const reducers: any = {};
function getNext<R>(): ReducerRegister<R> {
return <P extends R>(value: P, callback: Function) => {
reducers[value] = callback;
return getNextGlob<Exclude<R, P>>();
}
}
function getNextGlob<R>() {
return {
handlerRegister: getNext<R>()
};
}
callback(getNext<T>());
return reducers;
}
const enum Actions {
Action1 = 'action1',
Action2 = 'action2'
};
const reducers = createReducers<Actions>((handlerRegister) => {
handlerRegister(Actions.Action1, () => {}) // 'action1'|'action2'
.handlerRegister(Actions.Action2, () => {}) // 'action2'
.handlerRegister(); // Error -> 'never'
}); But you still cannot avoid your developer to perform something like: const reducers = createReducers<Actions>((handlerRegister) => {
handlerRegister(Actions.Action1, () => {}); // 'action1'|'action2'
handlerRegister(Actions.Action2, () => {}); // 'action1'|'action2' again
handlerRegister(Actions.Action1, () => {}); // 'action1'|'action2' again
}); |
Or... If you really want to constraint your developer, you can always use this approach: type NoNever<T, A> = T extends never ? never : A;
type _ReducerRegister<T> = {
register: NoNever<T, <P extends T>(value: P, callback: Function) => ReducerRegister<Exclude<T, P>>>;
commit: () => any;
};
type ReducerRegister<T> = OmitByValue<_ReducerRegister<T>, never>;
function createReducers<T>() {
const reducers: any = {};
const commit = () => {
return reducers;
};
function getNext<R>(): ReducerRegister<R> {
return {
// @ts-ignore
register: <P extends R>(value: P, callback: Function) => {
reducers[value] = callback;
return getNext<Exclude<R, P>>();
},
commit
};
}
return getNext<T>();
}
const enum Actions {
Action1 = 'action1',
Action2 = 'action2'
};
const reducers = createReducers<Actions>()
.register(Actions.Action1, () => { }) // 'action1'|'action2'
.register(Actions.Action2, () => { }) // 'action2'
.commit(); // register is not available anymore Here you can avoid at least the duplicate registration of your |
@IssueHunt has funded $50.00 to this issue.
|
@piotrwitek et. al I was able to find an approach that seems to work using a conditional type for the type Action = ActionType<typeof actions>;
type ReducerMap<S, A> = A extends PayloadAction<infer T, infer P>
? { [key in T]: (state: S, action: PayloadAction<T, P>) => S }
: never;
function createReducer<S, A extends { type: string }>(
initialState: S,
handlers: ReducerMap<State, Action>,
) {
return function reducer(state = initialState, action: A): S {
if (handlers.hasOwnProperty(action.type)) {
return (handlers as any)[action.type](state, action);
} else {
return state;
}
};
} EDIT for some context: This accomplishes linking the specific action type key to the specific action, therefore in your reducer map object it will automatically differentiate, eg., payload types. The tradeoff is that the EDIT 2: @piotrwitek I guess I should ask before assuming... do you want the |
Hey @joefiorini 🙂 I don't know why you need the /**
* Create a reducers from a defined handlerMap
* @param initialState
* @param handlersMap
*/
export function createReducers<T extends ActionType<ActionCreator<StringType>>, S>(
initialState: S,
handlersMap: {
[P in GetActionType<T>]?: ActionReducer<S, T, P>;
}
) {
return function <P extends GetActionType<T>>(state: S = initialState, action: GetAction<T, P>) {
if (handlersMap.hasOwnProperty(action.type)) {
return handlersMap[action.type]!(state, action);
}
return state;
}
} Then, this solution works perfectly as soon as you specify explicitly the createReducers<ExampleStoreActionsType, ExampleStoreState>(ExampleStoreState, {
"Example/ActionWithPayload": (state, action) => {
action.payload.username
return {
...state,
username: action.payload.username,
password: action.payload.password
};
}
}); But the thing is: I really don't like the createReducers<ExampleStoreActionsType, ExampleStoreState>(ExampleStoreState, {
[getType(ExampleStoreActions.ActionWithPayload)]: (state, action) => {
action.payload.username
return {
...state,
username: action.payload.username,
password: action.payload.password
};
}
}); But it's because of a limitation of TypeScript. That's why we created an issue in their side: microsoft/TypeScript#29718 So don't hesitate to upvote, and share this issue with lot of people, as the TypeScript team can reconsider the priority of this feature. |
@joefiorini I have tested your implementation but it doesn't fix the issues we had with my previous Could you tell us what do you think are the benefits of your solution and how it is relevant for this library? If action helpers still don't work (as @MasGaNo mentionted) which is an actual deal breaker here. |
@piotrwitek Totally missed the goal of having it work with the This solves my biggest problem which is having the exact I'm going to try out @MasGaNo's solution in my app, since that seems a bit simpler than mine, and if it solves the same problems I'll use that. |
@piotrwitek To help clear up anyone else getting confused about this issue, what do you think of renaming it to something along the lines of "Support getType helper in typesafe reducer factories" since this is about more than just "adding a typesafe reducer factory using object map"? |
I have a new API proposal ready for this feature, here are the highlights of new main features: import * as actions from './'
// NEW FEATURE: extend internal typesafe-actions RootAction type
// so you never have to use generic types in your application 😮
// it's as simple as below one-liner
declare module 'typesafe-actions' {
export type RootAction = ActionType<typeof actions>;
}
// now just import helper functions from "typesafe-actions" as always
import { createReducer, getType } from 'typesafe-actions'
const initialState = 0;
// Notice here you don't need to provide Action type to createReducer (magic!),
// because it's derived from the above declare section 😉
export const counterReducer = createReducer(initialState)
// you can pass single or multiple action [using array] creators instances
// and the action arg will be constrained in reducer function
.handleAction(actions.add, (state, action) => {
return state;
})
// it also works with type constants, here 'ADD' constant will not be accepted because it's handled above (removing handled actions from accepted argument)
.handleAction('INCREMENT', (state, action) => {
return state + 1;
})
//.handleAction <= exshausive checking! you cannot use "handleAction" anymore because all available actions are handled
counterReducer(0, increment()); // => 1
counterReducer(0, add(4)); // => 4 |
Extended proposal with a new feature to create reducer factory using object map, you can even compose different reducers together with type complete safety: const counterReducer = createReducer(initialState)
.handleAction(add, (state, action) => state + action.payload)
.handleAction(increment, (state, _) => state + 1);
const rootReducer = createReducer(initialState, {
ADD: counterReducer.reducers.ADD,
INCREMENT: counterReducer.reducers.INCREMENT,
}) All is already implemented and extensively tested, so should be rock solid and production ready. I just need to add documentation and examples and will release today 🚀 |
Hey @piotrwitek Hehe, you reused finally chaining-handle approach 🙂 Ok, I'm waiting for your release, and I will try it on my-side. I'm still more in favor of the objectMapCreator approach, but until TypeScript implement the missing feature, it could be definitely a good approach, for the Typing safety. // NEW FEATURE: extend internal typesafe-actions RootAction type
// so you never have to use generic types in your application 😮
// it's as simple as below one-liner
declare module 'typesafe-actions' {
export type RootAction = ActionType<typeof actions>;
} Really nice 👍 I never consider this approach! Will make some try with that 😄 |
Hey @MasGaNo, I think you missed a very important point, I made an implementation that is actually covering both approaches :) There is the second optional argument in createReducer to provide objectMapCreator, take a look again this time the simple implementation, everything is completely typesafe ⚔️🛡, and you can chain it even further with additional handlers. It's basically an extension mechanism. const rootReducer = createReducer(initialState, {
ADD: (state, action) => state + action.payload,
INCREMENT: (state, _) => state + 1,
}) What do you think? I worked across all typescript issues and it's even working with getType helper |
Yes right 😄 Very exciting to be able to test it! Unfortunately, I cannot do it now, need to wait tomorrow 😅 I really like the Array approach to solve the problem of "multi-case" from the "switch... case" approach. Currently, I store the reducerHandler into a variable and I pass it, but here, it's really a nice approach 👍 And you are still opened to let developer modify the generic parameters So yeah, definitely, looks very good! 👍 I will definitely provide you my feedback tomorrow 👍 |
<!-- Thank you for your contribution! 👍 --> <!-- Please makes sure that these checkboxes are checked before submitting your PR, thank you! --> ## Description Added createReducer - a new API to easily create typesafe reducers Example refactoring of regular reducer to createReducer: cef1a51 ## Related issues: - Resolved #106 - Resolved #123 ## Checklist * [x] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) * [x] I have linked all related issues above * [x] I have rebased my branch For bugfixes: * [ ] I have added at least one unit test to confirm the bug have been fixed For new features: * [x] I have updated API docs and tutorial (if applicable) * [x] I have added short examples to demonstrate new feature usage * [x] I have added type unit tests with `dts-jest` * [x] I have added runtime unit tests with `dts-jest` * [x] I have added usage example in codesandbox project
@piotrwitek has rewarded $35.00 to @piotrwitek. See it on IssueHunt
|
First thank you for this project, it's very cool and help us to reduce dramatically the number of files, lines of code, increase the readability and enforce the definition 👍
Is your feature request related to a problem?
In some of our projects we have very big reducers with big
switch
statement.So that's why I want to have this kind of approach from Redux documentations:
This approach helps us to reduce the cyclomatic complexity of the file, and allow us to test easily each reducer.
Describe a solution you'd like
In the same way I want to enforce the definition based on
typesafe-actions
, I want to have a way to create the reducers based on the list ofactions
created with the differentcreateAction
method and to get the correctstate
andaction
on eachreducer
.Who does this impact? Who is this for?
Well... for everybody I guess.
Describe alternatives you've considered
I made a try, but it wasn't fully a success... So if you can help me to complete this approach and/or to improve it, it will be great.
To illustrate my purpose, I prepared a demo-project here. I have 2 main concerns:
create-reducers.ts: I get a transpilation error. It's not necessary related totypesafe-actions
, so I will continue to investigate.constant
approach. So it work like a charm withconstant
even if I created the actions withcreateStandardAction
, but if I try to deal withgetType
, the magic disappear.Additional context
I know that I definitely need to improve my skills about Advanced Definition, so probably,
type
definition defined in thecreate-reducers.ts
file can easily be improved to manage the different use-case.Don't hesitate to tell me if you find this discussion appropriate or not here.
Thanks.
IssueHunt Summary
piotrwitek has been rewarded.
Backers (Total: $50.00)
Submitted pull Requests
Tips
IssueHunt has been backed by the following sponsors. Become a sponsor
The text was updated successfully, but these errors were encountered: