Skip to content
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

Merged
merged 12 commits into from
Feb 26, 2016
60 changes: 60 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export interface Action {
type: string;
Copy link
Collaborator Author

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 be any?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe

export interface Action<T> {
    type: T;
}

Copy link
Collaborator Author

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:

enum ActionType {
   A, B, C
}

But where would it be useful? We can use it in reducer:

function reducer(state, action: Action<ActionType>) {
  // ...
}

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.

}

export type Reducer<S> = (state: S, action: Action) => S;

export type Dispatch = (action: any) => any;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if try to not use too much any - it's just like working without support of typscript… :(

Consider more strict approach like:

interface Action {
    type: any;
}

interface Reducer<S> extends Function {
    <S>(state: S, action: Action): S;
}

interface ReducersMapObject<S> {
    [key: string]: Reducer<S>
}

interface Dispatch extends Function {
    <A extends Action>(action: A): A;
}

Or even better

interface Action<T> {
    type: T;
}

interface Reducer<S, T> extends Function {
    <S>(state: S, action: Action<T>): S;
}
…
interface Dispatch<T> extends Function {
    <A extends Action<T>>(action: A): A;
}

so anyone can implement his own strict actions:

enum ActionType {A, B, C}
interface MyAction implements Redux.Action<ActionType> {
    type: ActionType;
    payload: MyPayloadType;

or dynamic ones:

interface MyAction implements Redux.Action<any> {}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 Action.

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't avoid any here. Middleware potentially allows dispatch to accept anything: functions (thunks), promises, etc. Its return type could also be anything.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can. Just make it generic:

interface Dispatch extends Function {
    <A>(action: A): A;
}

or if dispatch returns output changed by middleware:

interface Dispatch extends Function {
    <A, O>(action: A): O;
}

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…

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 Dispatch type with constraints that fit you needs:

type MyDispatch = (action: MyAction) => MyAction;

Then you can use your strict MyDispatch in place of Dispatch anywhere and it will type-check correctly.

IMO if we add type parameters here, we'd have to write Dispatch<any> most of the time, because concrete type constraints depend on the middleware(s) you use.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 Store definition (no middleware) would be:

interface Store<S> {
  dispatch: Dispatch<Action, Action>;
}

Then we can strengthen Middleware type something like:

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 dispatch after all store enhancers were applied.
I need to experiment with this a bit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
    }
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 redux-thunk:

store.dispatch(dispatch => 42)  // returns 42

After applying thunk middleware dispatch gets type:

type ThunkDispatch = <O>(thunk: (dispatch, getState) => O) => O;
typeof store.dispatch == DefaultDispatch | ThunkDispatch;

Things get worse when we try to define types for thunk arguments. In case of single thunk middleware it is obviously DefaultDispatch:

type ThunkDispatch =
  <O, S>(thunk: (dispatch: DefaultDispatch, getState: () => S) => O) => O;

But what if we had e.g. Promise middleware in front of thunk middleware? We should be able to dispatch promises from thunk:

type PromiseDispatch = <P extends Promise<any>>(promise: P): P;
type ThunkDispatch =
  <O, S>(thunk: (dispatch: DefaultDispatch | PromiseDispatch, getState: () => S) => O) => O;

This means that thunk middleware should know about previously applied middlewares.

Copy link

Choose a reason for hiding this comment

The 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>() 

Copy link

Choose a reason for hiding this comment

The 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);

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes more sense for me now. Thanks.
So basically we don't constraint types of dispatch argument and return value, but add ability for user to specify them if they want?

Copy link

Choose a reason for hiding this comment

The 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;
}
Copy link

Choose a reason for hiding this comment

The 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 Middleware will always have to be parametrised with both type parameters. We can avoid this ( but I'm NOT sure if we should ) in the same manner as with Dispatch:

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… ;)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try to implement thunk middleware in TypeScript as an example. Start without any types:

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 thunk, so dispatch here can potentially accept anything, e.g. promises. Same for next, same for action.
Middleware is standalone and doesn't know anything about dispatch type prior to when it was applied.

Copy link

Choose a reason for hiding this comment

The 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 ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shows that elasticity of thunkMiddleware stays in oposition to type safety. Anyway it can be always done in that way:

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)
    }

;)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is — thunkMiddleware is standalone and it can't have knowledge of what MyStore 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;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once more, action creator here is anything that can be bound to Dispatch, so its return type is anything that Dispatch can accept.

}

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;
Copy link

Choose a reason for hiding this comment

The 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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "Predictable state container for JavaScript apps",
"main": "lib/index.js",
"jsnext:main": "es/index.js",
"typings": "./index.d.ts",
"files": [
"dist",
"lib",
Expand Down