-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Add typings #538
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 typings #538
Conversation
} | ||
|
||
interface MergeProps<TStateProps, TDispatchProps, TOwnProps> { | ||
(stateProps: TStateProps, dispatchProps: TDispatchProps, ownProps: TOwnProps): TStateProps & TDispatchProps; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not correct. mergeProps
can return anything, it should be a type parameter of MergeProps
as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
* - TDispatchProps: Result of MapDispatchToProps | ||
* - TOwnProps: Props passed to the wrapping component | ||
*/ | ||
export function connect<State, TOwnProps>(): ComponentDecorator<{ dispatch: Dispatch<State> }, TOwnProps>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it shouldn't replace props with {dispatch}
, but merge them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what ComponentDecorator is doing, no issue here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't. ComponentDecorator in this case states that it attepts component with props {dispatch: Dispatch<State>}
and returns component with props TOwnProps
.
In reality the effect of connect
is adding dispatch
to other properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replied below
options?: Options | ||
): ComponentDecorator<TStateProps & TDispatchProps, TOwnProps>; | ||
|
||
interface MapStateToProps<State, TStateProps, TOwnProps> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not have type parameters in the same order as in the signature?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
|
||
export function connect<State, TOwnProps, TStateProps>( | ||
mapStateToProps: MapStateToProps<State, TStateProps, TOwnProps>, | ||
): ComponentDecorator<TStateProps & { dispatch: Dispatch<State> }, TOwnProps>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrapped component should get all of TStateProps
, TOwnProps
and dispatch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as previous one. ComponentDecorator exposes the exported component with TOwnProps
outside, with TStateProps & { dispatch: Dispatch<State> }
satisfied by React-Redux.
I think you are possibly misreading what ComponentDecorator
does or how which props of the HOC should be exposed outside.
I've tested that part extensively in my (big) app and it really behaves as supposed to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const connector = connect<S, {foo: string}, {bar: number}>(...);
Connector in this case gets the type ComponentDecorator<{bar: number} & {dispatch: Dispatch}, {foo: string}>
. The component we pass in would have props {bar: number} & {dispatch: Dispatch}
. The component that comes out would have props {foo: string}
.
But how connect
really works is this: connected component accepts {foo: string}
— that's correct. But the wrapped component will get all of {foo: string}
, {bar: number}
and {dispatch: Dispatch}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. I felt that not exposing state props and dispatch prop to parent component was actually a feature.
Is there a valid use case where we should let users overwrite HOC's props?
Or is it more that you feel type checking should try to stick to reality as much as possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do this:
ComponentDecorator<TStateProps & { dispatch: Dispatch<State> }, TStateProps & { dispatch?: Dispatch<State> } & TOwnProps>
We can make dispatch optional but not TStateProps.
User would have to make all its StateProps optional so that it won't be required when component is used in parent.
But semantically, TStateProps are not optional.
This is a wontfix IMO.
): ComponentDecorator<TStateProps & { dispatch: Dispatch<State> }, TOwnProps>; | ||
|
||
export function connect<State, TOwnProps, TStateProps, TDispatchProps>( | ||
mapStateToProps: MapStateToProps<State, TStateProps, TOwnProps>|null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make another signature for null
as first argument. We'd get stronger typings for this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another signature for null as first argument
Sorry I don't get it.
Should I add a type that expresses the union of MapStateToProps<State, TStateProps, TOwnProps>
and null
?
What advantage would it have?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I propose adding this instead of union type:
export function connect<State, TOwnProps, TDispatchProps>(
mapStateToProps: null,
mapDispatchToProps: ...
)
This is stronger because in this case TStateProps
isn't variable, the only type it could be is {}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh. Ok, sorry. Makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
): ComponentDecorator<TStateProps & TDispatchProps, TOwnProps>; | ||
|
||
export function connect<State, TOwnProps, TStateProps, TDispatchProps>( | ||
mapStateToProps: MapStateToProps<State, TStateProps, TOwnProps>|null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mapState
/ mapDispatch
can be factory functions. See https://github.com/reactjs/reselect/tree/v2.5.3#sharing-selectors-with-props-across-multiple-components
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
/** | ||
* Any since not every ActionCreator returns the same Action | ||
*/ | ||
interface MapDispatchToPropsObject<TDispatchProps> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be used as a possible type for mapDispatch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This have been removed totally as well.
* Any since not every ActionCreator returns the same Action | ||
*/ | ||
interface MapDispatchToPropsObject<TDispatchProps> { | ||
[name: string]: ActionCreator<TDispatchProps>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not true, type argument of ActionCreator is the type of created Action. Here it could be anything
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This have been removed totally as well.
*/ | ||
export interface ProviderProps { | ||
store?: Store<any>; | ||
children?: ReactNode; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not needed as children
prop is automatically added to components defined as classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good. Let's fix this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
* generic parameter in Store here by using any as the type | ||
*/ | ||
export interface ProviderProps { | ||
store?: Store<any>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it really optional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess not. This should be fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
import { Store, Dispatch, ActionCreator } from 'redux'; | ||
|
||
interface ComponentDecorator<TOriginalProps, TOwnProps> { | ||
(component: ComponentClass<TOriginalProps>|StatelessComponent<TOriginalProps>): ComponentClass<TOwnProps>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found out that it's better to have StatelessComponent
as the first type. Otherwise when you pass SFC typescript will think that it is a constructor function for component class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
That's not generally true, the pattern I use everywhere is this: type State = {foo: string};
const Component = connect(
(state: State) => {
return {
bar: state.foo,
};
}
)(props => {
props.bar // inferred as `string`
}) This saves a lot of typing clutter. Even if you connect component classes, you can omit type parameters if only you annotate arguments for const connector = connect(
(state: MyState, props: MyProps) => {...}
) This way |
const connector = connect(
(state: MyState, props: MyProps) => {...}
) You are not type checking StateProps return. If you typo your hash, you won't know it. Thus you actually want: const connector = connect(
(state: MyState, props: MyProps): StateProps => {...}
) But then why not simply const connector<MyState, MyProps, StateProps> = connect(
(stats, props) => {...}
) The advantage is you get an error if you forget one of the generic argument, whereas you might forget to type check return types, argument types and lose some type checking, hence my warning, that was mostly directed to newcomers. |
…sh of action creators
Job's done! 🎉 |
Can this actually get built against the |
That's just a matter of taste. I prefer not to specify an interface for an object that is consumed just a few lines later. My point is that you don't have to always specify type parameters because TS is quite good in inferring return types.
That's not what I meant. The right one is: ComponentDecorator<TStateProps & {dispatch: Dispatch<State>} & TOwnProps, TOwnProps>; Own props passed from outside, State props and |
* - TStateProps: Result of MapStateToProps | ||
* - TDispatchProps: Result of MapDispatchToProps | ||
*/ | ||
export function connect<State, TOwnProps>(): ComponentDecorator<{ dispatch: Dispatch<State> }, TOwnProps>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct return type:
ComponentDecorator<{dispatch: Dispatch<State>} & TOwnProps, TOwnProps>;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
export function connect<State, TOwnProps, TStateProps>( | ||
mapStateToProps: FuncOrSelf<MapStateToProps<State, TOwnProps, TStateProps>>, | ||
): ComponentDecorator<TStateProps & { dispatch: Dispatch<State> }, TOwnProps>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct return type:
ComponentDecorator<TStateProps & {dispatch: Dispatch<State>} & TOwnProps, TOwnProps>;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
export function connect<State, TOwnProps, TStateProps, TDispatchProps>( | ||
mapStateToProps: FuncOrSelf<MapStateToProps<State, TOwnProps, TStateProps>>, | ||
mapDispatchToProps: FuncOrSelf<MapDispatchToPropsFunction<State, TOwnProps, TDispatchProps> | MapDispatchToPropsObject & TDispatchProps> | ||
): ComponentDecorator<TStateProps & TDispatchProps, TOwnProps>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct return type:
ComponentDecorator<TStateProps & TDispatchProps & TOwnProps, TOwnProps>;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
|
||
export function connect<State, TOwnProps, TStateProps, TDispatchProps>( | ||
mapStateToProps: FuncOrSelf<MapStateToProps<State, TOwnProps, TStateProps>>, | ||
mapDispatchToProps: FuncOrSelf<MapDispatchToPropsFunction<State, TOwnProps, TDispatchProps> | MapDispatchToPropsObject & TDispatchProps> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a case of passing object to mapDispatchToProps
, what is TDispatchProps
? What's the difference with MapDispatchToPropsObject
?
I think it should be a different signature for the case when mapDispatchToProps
is object, something like this:
function connect<State, TOwnProps, TStateProps, TDispatchProps extends MapDispatchToPropsObject>(
mapStateToProps: FuncOrSelf<MapStateToProps<State, TOwnProps, TStateProps>>,
mapDispatchToProps: TDispatchProps
): ComponentDecorator<TStateProps & TDispatchProps & TOwnProps, TOwnProps>;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TDispatchProps
may be set by the user. It's the list of dispatchable action creators he intends to use in the component.
MapDispatchToPropsObject
is a constraint we add to make sure values of the hash are action creators.
Adding a different signature is problematic because the combination will be very large. Same issue as with mapStateToProps: null
.
export function connect<State, TOwnProps, TDispatchProps>( | ||
mapStateToProps: null, | ||
mapDispatchToProps: FuncOrSelf<MapDispatchToPropsFunction<State, TOwnProps, TDispatchProps> | MapDispatchToPropsObject & TDispatchProps> | ||
): ComponentDecorator<TDispatchProps, TOwnProps>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct return type:
ComponentDecorator<TDispatchProps & TOwnProps, TOwnProps>;
Also see comment above on the case when mapDispatchToProps
is object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
mapDispatchToProps: FuncOrSelf<MapDispatchToPropsFunction<State, TOwnProps, TDispatchProps> | MapDispatchToPropsObject & TDispatchProps>, | ||
mergeProps: MergeProps<TOwnProps, TStateProps, TDispatchProps, TMergeProps>, | ||
options?: Options | ||
): ComponentDecorator<TStateProps & TDispatchProps, TOwnProps>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct return type:
ComponentDecorator<TMergeProps, TOwnProps>;
Also see comment above on the case when mapDispatchToProps is object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
} | ||
|
||
interface MergeProps<TOwnProps, TStateProps, TDispatchProps, TMergeProps> { | ||
(ownProps: TOwnProps, stateProps: TStateProps, dispatchProps: TDispatchProps): TMergeProps; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order of arguments is incorrect, the right one would be: stateProps, dispatchProps, ownProps
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh my... I thought I was dealing with named arguments...
Fixed
@bbenezech Can you rebase against |
Looks like everything's been addressed.
@bbenezech Looks like there's a conflict because the git branch isn't based on |
@timdorr I can't change my own branch in edit. I opened a new PR. |
See #433 for discussion.
I tried using DT's but I found some issues (connect with no argument was not adding dispatch prop, and some API calls where not possible). I fixed the issues for the most common use cases, described below.
The rest of the API should work like it does with DT version.
Please note: always define templates for your connect calls or you will have issues/subpar type checking (inference engine will replace non-inferable templates type with
{}
).API Ref that we should targeted as much as possible: https://github.com/reactjs/react-redux/blob/master/docs/api.md#examples
These are the supported use cases that I tested and that 100% type check without any error or leniency:
1. Inject
dispatch
and don't listen to storeAppState
is used as a template for the Dispatch type, as required by redux typingsOwnProps
exposes the component with its props signature, without complicatedInferableComponentDecorator
that won't allow me to inject dispatcher.2. Hydrate action creators without subscribing to the store
3. Inject
dispatch
and props from global state4. Inject props from global state and hydrate action creators
AppState
describes your App's stateOwnProps
describes your component's outside props (the ones that the parent component should specify)StateProps
describes the component's props that come from the global stateDispatchProps
describes the component's props that come from hydrated action creatorsHonestly, I only use case (3.)
This should get us started. Please feedback!