-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
Revamp TypeScript typing with more type safety #2563
Changes from 1 commit
0dd93d1
299b0b7
a76e708
813fbff
4f5c320
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
|
||
/** | ||
* An *action* is a plain object that represents an intention to change the | ||
* state. Actions are the only way to get data into the store. Any data, | ||
|
@@ -12,12 +13,13 @@ | |
* Other than `type`, the structure of an action object is really up to you. | ||
* If you're interested, check out Flux Standard Action for recommendations on | ||
* how actions should be constructed. | ||
* | ||
* @template T the type of the action's `type` tag. | ||
*/ | ||
export interface Action { | ||
type: any; | ||
export interface Action<T = any> { | ||
type: T; | ||
} | ||
|
||
|
||
/* reducers */ | ||
|
||
/** | ||
|
@@ -41,15 +43,18 @@ export interface Action { | |
* | ||
* *Do not put API calls into reducers.* | ||
* | ||
* @template S State object type. | ||
* @template S The type of state consumed and produced by this reducer. | ||
* @template A The type of actions the reducer can potentially respond to. | ||
*/ | ||
export type Reducer<S> = <A extends Action>(state: S | undefined, action: A) => S; | ||
export type Reducer<S = {}, A extends Action = Action> = (state: S | undefined, action: A) => S; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still, do we really want to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was trying to keep this PR as backwards-compatible as possible, but if that's not important I'm more than happy to remove it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I don't get how it aids backward compatibility. Previous There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, I appear to have been confused when I made this comment. I was thinking of the state parameter to the The argument for defaulting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, this makes sense. The only thing that bothers me: if the state type of reducer includes A very simplified example: type Reducer<S = {}> = (state: S | undefined) => S;
const r: Reducer = (state: null | undefined = null): null => {
return state;
} Gives
Works fine after setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you're right, except that As far as I know, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remember though, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not quite the same, because for example declare x: {} | null | undefined
x.foo // this is a type error
declare y: any
y.foo // this is fine
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The definition covers this since the state argument is of type There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You are right, I used the wrong word. They are equivalent in terms of assignability. |
||
|
||
/** | ||
* Object whose values correspond to different reducer functions. | ||
* | ||
* @template A The type of actions the reducers can potentially respond to. | ||
*/ | ||
export type ReducersMapObject<S> = { | ||
[K in keyof S]: Reducer<S[K]>; | ||
export type ReducersMapObject<S = {}, A extends Action = Action> = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Default for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having defaults means you can just reference There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes some sense. But wouldn't There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You raise a good point and I think it means I chose the wrong default... instead it should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think that should work. |
||
[K in keyof S]: Reducer<S[K], A>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already done on the This PR should probably target the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I'll do that. |
||
} | ||
|
||
/** | ||
|
@@ -70,7 +75,7 @@ export type ReducersMapObject<S> = { | |
* @returns A reducer function that invokes every reducer inside the passed | ||
* object, and builds a state object with the same shape. | ||
*/ | ||
export function combineReducers<S>(reducers: ReducersMapObject<S>): Reducer<S>; | ||
export function combineReducers<S, A extends Action>(reducers: ReducersMapObject<S, A>): Reducer<S, A>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a default type for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
|
||
/* store */ | ||
|
@@ -92,9 +97,12 @@ export function combineReducers<S>(reducers: ReducersMapObject<S>): Reducer<S>; | |
* function to handle async actions in addition to actions. Middleware may | ||
* transform, delay, ignore, or otherwise interpret actions or async actions | ||
* before passing them to the next middleware. | ||
* | ||
* @template S unused, here only for backwards compatibility. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should put more thought into this. These changes are already breaking, so why should we bother about compatibility here? The But in real-world it appeared that we rarely use const mapDispatchToProps = (dispatch: Dispatch<MyState>) => ... For users who don't use We could just remove Otherwise we should make this docstring point to the use case of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I said I'm happy to remove the state parameter and I definitely think this should be done at some point... If you think this PR is the right place for it I'll remove it. |
||
* @template D the type of things (actions or otherwise) which may be dispatched. | ||
*/ | ||
export interface Dispatch<S> { | ||
<A extends Action>(action: A): A; | ||
export interface Dispatch<S = any, D = Action> { | ||
<A extends D>(action: A): A; | ||
} | ||
|
||
/** | ||
|
@@ -109,9 +117,11 @@ export interface Unsubscribe { | |
* There should only be a single store in a Redux app, as the composition | ||
* happens on the reducer level. | ||
* | ||
* @template S State object type. | ||
* @template S The type of state held by this store. | ||
* @template A the type of actions which may be dispatched by this store. | ||
* @template N The type of non-actions which may be dispatched by this store. | ||
*/ | ||
export interface Store<S> { | ||
export interface Store<S = {}, A extends Action = Action, N = never> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, the default for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See previous comment... if that doesn't sway you, I'll remove the default. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dispatching non-actions is handled by augmenting the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So are you advocating removal of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please don't. Augmenting Dispatch for things like redux-thunk is broken, see reduxjs/redux-thunk#82. Still no one gave a solution how typing middlewares can work when redux-thunk has augmented Dispatch. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that module augmentation is more of a workaround rather than an ideal solution. Still, it gives us something that we can't achieve with just extending the type of Also, from what I see in the issue, the error comes from the definition of Considering the ideal solution: adding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sounds good to me. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After thinking a bit more about it, I don't think that improves it. Using a union type for the Action and a union type for the return value does not really help here (e.g. with redux-thunk). What you need instead is two different signatures (overloaded functions). I'm thinking the Action shouldn't be the type param (for Store and Middleware) but instead the whole Dispatch should be. |
||
/** | ||
* Dispatches an action. It is the only way to trigger a state change. | ||
* | ||
|
@@ -138,7 +148,7 @@ export interface Store<S> { | |
* Note that, if you use a custom middleware, it may wrap `dispatch()` to | ||
* return something else (for example, a Promise you can await). | ||
*/ | ||
dispatch: Dispatch<S>; | ||
dispatch: Dispatch<any, A | N>; | ||
|
||
/** | ||
* Reads the state tree managed by the store. | ||
|
@@ -182,7 +192,7 @@ export interface Store<S> { | |
* | ||
* @param nextReducer The reducer for the store to use instead. | ||
*/ | ||
replaceReducer(nextReducer: Reducer<S>): void; | ||
replaceReducer(nextReducer: Reducer<S, A>): void; | ||
} | ||
|
||
/** | ||
|
@@ -191,11 +201,13 @@ export interface Store<S> { | |
* `createStore(reducer, preloadedState)` exported from the Redux package, from | ||
* store creators that are returned from the store enhancers. | ||
* | ||
* @template S State object type. | ||
* @template S The type of state to be held by the store. | ||
* @template A The type of actions which may be dispatched. | ||
* @template D The type of all things which may be dispatched. | ||
*/ | ||
export interface StoreCreator { | ||
<S>(reducer: Reducer<S>, enhancer?: StoreEnhancer<S>): Store<S>; | ||
<S>(reducer: Reducer<S>, preloadedState: S, enhancer?: StoreEnhancer<S>): Store<S>; | ||
<S, A extends Action, N>(reducer: Reducer<S, A>, enhancer?: StoreEnhancer<N>): Store<S, A, N>; | ||
<S, A extends Action, N>(reducer: Reducer<S, A>, preloadedState: S, enhancer?: StoreEnhancer<N>): Store<S, A, N>; | ||
} | ||
|
||
/** | ||
|
@@ -215,10 +227,11 @@ export interface StoreCreator { | |
* provided by the developer tools. It is what makes time travel possible | ||
* without the app being aware it is happening. Amusingly, the Redux | ||
* middleware implementation is itself a store enhancer. | ||
* | ||
*/ | ||
export type StoreEnhancer<S> = (next: StoreEnhancerStoreCreator<S>) => StoreEnhancerStoreCreator<S>; | ||
export type GenericStoreEnhancer = <S>(next: StoreEnhancerStoreCreator<S>) => StoreEnhancerStoreCreator<S>; | ||
export type StoreEnhancerStoreCreator<S> = (reducer: Reducer<S>, preloadedState?: S) => Store<S>; | ||
export type StoreEnhancer<N = never> = (next: StoreEnhancerStoreCreator<N>) => StoreEnhancerStoreCreator<N>; | ||
export type GenericStoreEnhancer<N = never> = StoreEnhancer<N>; | ||
export type StoreEnhancerStoreCreator<N = never> = <S = any, A extends Action = Action>(reducer: Reducer<S, A>, preloadedState?: S) => Store<S, A, N>; | ||
|
||
/** | ||
* Creates a Redux store that holds the state tree. | ||
|
@@ -253,8 +266,8 @@ export const createStore: StoreCreator; | |
|
||
/* middleware */ | ||
|
||
export interface MiddlewareAPI<S> { | ||
dispatch: Dispatch<S>; | ||
export interface MiddlewareAPI<S = any, D = Action> { | ||
dispatch: Dispatch<any, D>; | ||
getState(): S; | ||
} | ||
|
||
|
@@ -268,7 +281,7 @@ export interface MiddlewareAPI<S> { | |
* asynchronous API call into a series of synchronous actions. | ||
*/ | ||
export interface Middleware { | ||
<S>(api: MiddlewareAPI<S>): (next: Dispatch<S>) => Dispatch<S>; | ||
<S = any, D = Action>(api: MiddlewareAPI<S, D>): (next: Dispatch<any, D>) => Dispatch<any, D>; | ||
} | ||
|
||
/** | ||
|
@@ -317,8 +330,8 @@ export interface ActionCreator<A> { | |
/** | ||
* Object whose values are action creator functions. | ||
*/ | ||
export interface ActionCreatorsMapObject { | ||
[key: string]: ActionCreator<any>; | ||
export interface ActionCreatorsMapObject<A = any> { | ||
[key: string]: ActionCreator<A>; | ||
} | ||
|
||
/** | ||
|
@@ -340,19 +353,19 @@ export interface ActionCreatorsMapObject { | |
* creator wrapped into the `dispatch` call. If you passed a function as | ||
* `actionCreator`, the return value will also be a single function. | ||
*/ | ||
export function bindActionCreators<A extends ActionCreator<any>>(actionCreator: A, dispatch: Dispatch<any>): A; | ||
export function bindActionCreators<A, C extends ActionCreator<A>>(actionCreator: C, dispatch: Dispatch<any, A>): C; | ||
|
||
export function bindActionCreators< | ||
A extends ActionCreator<any>, | ||
B extends ActionCreator<any> | ||
>(actionCreator: A, dispatch: Dispatch<any>): B; | ||
>(actionCreator: A, dispatch: Dispatch<any, any>): B; | ||
|
||
export function bindActionCreators<M extends ActionCreatorsMapObject>(actionCreators: M, dispatch: Dispatch<any>): M; | ||
export function bindActionCreators<A, M extends ActionCreatorsMapObject<A>>(actionCreators: M, dispatch: Dispatch<any, A>): M; | ||
|
||
export function bindActionCreators< | ||
M extends ActionCreatorsMapObject, | ||
N extends ActionCreatorsMapObject | ||
>(actionCreators: M, dispatch: Dispatch<any>): N; | ||
M extends ActionCreatorsMapObject<any>, | ||
N extends ActionCreatorsMapObject<any> | ||
>(actionCreators: M, dispatch: Dispatch<any, any>): N; | ||
|
||
|
||
/* compose */ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,4 +36,4 @@ const t11: number = compose(stringToNumber, numberToString, stringToNumber, | |
|
||
|
||
const funcs = [stringToNumber, numberToString, stringToNumber]; | ||
const t12 = compose(...funcs)('bar', 42, true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This appears to have just been a bug in the old test case, which the newer version of TypeScript caught. |
||
const t12 = compose(...funcs)('bar'); |
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.
Am I right that the intended use case for the type argument is using string literal types?
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.
Yes