-
-
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
Add TypeScript definitions #1413
Changes from 2 commits
889030c
c01501f
4b09a3b
6b4f7f6
b19ebc1
a6b4d80
56ab522
1fa6636
6b61fca
16325a2
6317727
a5d44fa
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 |
---|---|---|
@@ -0,0 +1,60 @@ | ||
export interface Action { | ||
type: string; | ||
} | ||
|
||
export type Reducer<S> = (state: S, action: Action) => S; | ||
|
||
export type Dispatch = (action: any) => any; | ||
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 would be great if try to not use too much Consider more strict approach like:
Or even better
so anyone can implement his own strict actions:
or dynamic ones:
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. Why isn't this sufficient? interface Action {
type: string;
} If you want to have strict actions you can just do: enum ActionType {A, B, C}
interface MyAction<P> {
type: ActionType;
payload: P;
} And it will be assignable to Also, reducer should not be constrained to accept only some selected actions. It should accept any possible action and bypass ones it doesn't need to handle. 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. We can't avoid 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. We can. Just make it generic:
or if dispatch returns output changed by middleware:
This is one of typings that is necessary for strict typing of any redux based app. If dispatch can return anything and can't be parametrised then static typing is of no value here… 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. How do we use it? Considering first option we'd have: export interface Store<S> {
dispatch: Dispatch<any>;
// ...
} So the only place where type parameters would be useful is the app code, e.g. @connect(
state => /* ... */,
(dispatch: Dispatch<Action>) => {
return {
someActionCreator: () => dispatch(someActionCreator)
}
}
) But nothing prevents you to set up custom type MyDispatch = (action: MyAction) => MyAction; Then you can use your strict IMO if we add type parameters here, we'd have to write 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 see how your proposal can be useful: if we put type Dispatch<A, O> = (action: A) => O; Then original interface Store<S> {
dispatch: Dispatch<Action, Action>;
} Then we can strengthen interface Middleware {
<S, A, O>(api: MiddlewareAPI<S>):
<D extends Dispatch<any, any>>(next: D) => D | Dispatch<A, O>;
} Thus we could always have strong typings 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. Here's what I've come up with: type StoreEnhancer<NewDispatch extends Dispatch<any, any>> =
<D extends Dispatch<any, any>>(next: StoreCreator<D>) =>
StoreCreator<D | NewDispatch>; It takes store creator with original dispatch and returns one where dispatch is either original or new. But I couldn't get it to work with enhancers that don't add new dispatch signature, e.g. logger middleware: const logger: Middleware</* ? */> = ({getState}) => {
return <D extends Dispatch<any, any>>(next: D) =>
(action: /* ? */) => {
console.log('will dispatch', action)
let returnValue = next(action)
console.log('state after dispatch', getState())
return returnValue
}
} 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. That's fine for default dispatch (no middleware): type DefaultDispatch = <A extends Action>(action: A) => A; The problem here is that dispatch doesn't always return its input. Consider store.dispatch(dispatch => 42) // returns 42 After applying thunk middleware type ThunkDispatch = <O>(thunk: (dispatch, getState) => O) => O;
typeof store.dispatch == DefaultDispatch | ThunkDispatch; Things get worse when we try to define types for type ThunkDispatch =
<O, S>(thunk: (dispatch: DefaultDispatch, getState: () => S) => O) => O; But what if we had e.g. Promise middleware in front of type PromiseDispatch = <P extends Promise<any>>(promise: P): P;
type ThunkDispatch =
<O, S>(thunk: (dispatch: DefaultDispatch | PromiseDispatch, getState: () => S) => O) => O; This means that 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. Tricky situation - JS (and other dynamically typed langs) encourages to build structures that accept anything and handle type inside (runtime reflection) while static typing is about splitting it to handle different inputs/outputs in separate functions/methods. Thunk (and probably many other) middleware is so elastic, that best solution is to use just: interface Dispatch extends Function {
<A>(action: A) => A;
<A, B>(action: A) => B;
} And use it with thunk: const dispatch: Dispatch;
…
… dispatch<any>() … 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. Also consider this: type ThunkDispatch =
<O, S>(thunk: (dispatch: DefaultDispatch | PromiseDispatch, getState: () => S) => O; is exactly same as: type Thunk<S, O> = (dispatch: Dispatch, getState: () => S) => O;
type ThunkDispatch = <S, O>(thunk: Thunk<S, O>) => O; Which could be replaced with: type ThunkDispatch = <Th, O>(thunk: Th) => O;
// Just set type parameter `Th` as `Thunk<SomeState, TheOutput>` and `O` as `Output` Which is exactly same as second overload here (: interface Dispatch extends Function {
<A>(action: A) => A;
<A, B>(action: A) => B;
} Proposed promise dispatch is actually dispatch: type PromiseDispatch = <P extends Promise<any>>(promise: P): P;
// what is
const dispatch: Dispatch;
… dispatch<Promise<any>>(promise); 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 makes more sense for me now. Thanks. 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. Right. I think it's the best we can do - will satisfy both strict typing freaks (like me) and elasticity lovers. I think the overloaded one does the job in a best possible way: interface Dispatch extends Function {
<A>(action: A): A;
<T, O>(action: T): O;
} |
||
|
||
export interface MiddlewareAPI<S> { | ||
dispatch: Dispatch; | ||
getState: () => S; | ||
} | ||
|
||
export interface Middleware { | ||
<S>(api: MiddlewareAPI<S>): (next: Dispatch) => 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. Middleware definition below seems to be enough, but… interface Middleware<S> extends Function {
(store: MiddlewareAPI<S>): (next: Dispatch) => Dispatch;
} It actually does not return a dispatch but function mapping input of type A to output of type B: interface Middleware<S, A, B> extends Function {
(store: MiddlewareAPI<S>): (next: Dispatch) => (action: A) => B;
} But in this case interface Middleware extends Function {
<S, A, B>(store: MiddlewareAPI<S>): (next: Dispatch) => (action: A) => B;
} It's not always so easy to add static type definitions to code written in dynamically typed language… ;) 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 try to implement const thunkMiddleware = ({dispatch, getState}) =>
(next) => (action) => {
return typeof action === function ? action(dispatch, getState) : next(action)
} Now what types can we add here? Keep in mind that there may me other middlewares applied before 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. // in our typings
interface Middleware<S, A, B> extends Function {
(store: MiddlewareAPI<S>): (next: Dispatch) => (action: A) => B;
} import { MyState } from './wherever-my-state-is-declared'
type ThunkAction = ((d: Dispatch, gs: () => MyState) => ThunkAction) | Object;
const thunkMiddleware: Middleware<MyStore, ThunkAction, ThunkAction> =
({dispatch, getState}) =>
(next) => (action) => {
return typeof action === function ? action(dispatch, getState) : next(action)
} Does it do the job ? 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 shows that elasticity of import { MyState } from './wherever-my-state-is-declared';
const thunkMiddleware: Middleware<MyStore, any, any> =
({dispatch, getState}) =>
(next) => (action) => {
return typeof action === function ? action(dispatch, getState) : next(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. The problem is — |
||
|
||
export interface Store<S> { | ||
dispatch: Dispatch; | ||
getState: () => S; | ||
subscribe: (listener: () => void) => () => void; | ||
replaceReducer: (reducer: Reducer<S>) => void; | ||
} | ||
|
||
export interface StoreCreator<S> { | ||
(reducer: Reducer<S>): Store<S>; | ||
(reducer: Reducer<S>, enhancer: StoreEnhancer): Store<S>; | ||
(reducer: Reducer<S>, initialState: S): Store<S>; | ||
(reducer: Reducer<S>, initialState: S, enhancer: StoreEnhancer): Store<S>; | ||
} | ||
|
||
export type StoreEnhancer = <S>(next: StoreCreator<S>) => StoreCreator<S>; | ||
|
||
export const createStore: StoreCreator; | ||
|
||
export function combineReducers<S>(reducers: {[key: string]: Reducer<any>}): Reducer<S>; | ||
export function applyMiddleware<S>(...middlewares: Middleware[]): StoreEnhancer; | ||
|
||
|
||
export interface ActionCreator { | ||
(...args: any[]): any; | ||
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. Once more, action creator here is anything that can be bound to |
||
} | ||
|
||
export function bindActionCreators<T extends ActionCreator|{[key: string]: ActionCreator}>(actionCreators: T, dispatch: Dispatch): T; | ||
|
||
// from DefinitelyTyped/compose-function | ||
// Hardcoded signatures for 2-4 parameters | ||
export function compose<A, B, C>(f1: (b: B) => C, | ||
f2: (a: A) => B): (a: A) => C; | ||
export function compose<A, B, C, D>(f1: (b: C) => D, | ||
f2: (a: B) => C, | ||
f3: (a: A) => B): (a: A) => D; | ||
export function compose<A, B, C, D, E>(f1: (b: D) => E, | ||
f2: (a: C) => D, | ||
f3: (a: B) => C, | ||
f4: (a: A) => B): (a: A) => E; | ||
|
||
// Minimal typing for more than 4 parameters | ||
export function compose<Result>(f1: (a: any) => Result, | ||
...functions: Function[]): (a: any) => Result; | ||
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 it can be even better if we decide what should be the input type of composed function: export function compose<R, I>(fn1: (arg: any) => R,
...functions: Function[]): (arg: I) => R;
// I for input, R for Result |
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 this correct? Do we expect action type to always be
string
, or should it beany
?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.
Maybe
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 only case I see for this is your example with
enum
:But where would it be useful? We can use it in reducer:
But that would be incorrect:
action
argument here is not only your user-defined actions, it can also be{type: '@@redux/INIT'}
or any other action used by third-party redux library.