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

Conversation

@aikoven
Copy link
Collaborator

commented Feb 16, 2016

Original issue: #1401

@gaearon

This comment has been minimized.

Copy link
Contributor

commented Feb 16, 2016

Do we want somebody else to review these too, or is this good to merge?

@ulfryk

This comment has been minimized.

Copy link

commented Feb 16, 2016

  1. I'd suggest adding tests for typings (like in DT)
  2. Please take a look at my proposition -> https://gist.github.com/ulfryk/69981ccfb488647b6ee3 .

for example

export function createStore<S>(reducer: Reducer<S>, initialState?: S, enhancer?: StoreEnhancer): Store<S>;

does not give strict rules of use, look at this instead:

interface IStoreCreator extends Function {
    <S>(reducer: IReducer<S>): Store<S>;
    <S>(reducer: IReducer<S>, enhancer: IStoreEnhancer<S>): Store<S>;
    <S>(reducer: IReducer<S>, initialState: S): Store<S>;
    <S>(reducer: IReducer<S>, initialState: S, enhancer: IStoreEnhancer<S>): Store<S>;
}
…
interface IStoreEnhancer<S> extends Function {
    (createStore: IStoreCreator): IStoreCreator;
}
…
const createStore: IStoreCreator;

etc. etc.

Of course I'm not talking about naming convention (ignore I prefix if you want). What is the most useful in my proposition - it's the strict set of rules how createStore() can be used.

@gaearon

This comment has been minimized.

Copy link
Contributor

commented Feb 16, 2016

If we can add tests for these, we should add tests.

index.d.ts Outdated

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

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

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 16, 2016

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

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 17, 2016

Author Collaborator

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.

index.d.ts Outdated
<S>(api: MiddlewareAPI<S>): (next: Dispatch) => Dispatch;
}

export class Store<S> {

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 16, 2016

It's not a class because you can not import it from Redux (or 'redux') lib…

Trying this:

import { Store } from "redux";
const myStore = new Store({myStorKeys:andValues});

or just

const myStore = new Redux.Store({myStorKeys:andValues});

will compile but throw an error in runtime - this is makes static typing a bit useless :(

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 17, 2016

Author Collaborator

Correct. Not sure how it became a class here.

@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Feb 16, 2016

@ulfryk As far as I can see from createStore source, enhanced store creator has only one signature:
https://github.com/reactjs/redux/blob/master/src%2FcreateStore.js#L49

return enhancer(createStore)(reducer, initialState)

But you are right about original createStore signatures. I'll make a fix.

You are also right about type constraint on action argument for Reducer.
But Dispatch can accept argument of any type, not only Action, if user uses some middleware.

@gaearon DefinitelyTyped uses TypeScript code to see if it compiles correctly against definitions. I've added tests for original PR: https://github.com/aikoven/DefinitelyTyped/blob/redux/redux/redux-tests.ts. But I'll need to figure out how to actually run them here.

Will do it tomorrow.

@ulfryk

This comment has been minimized.

Copy link

commented Feb 16, 2016

@aikoven

Enhanced creator

You're right - enhancer has only one signature. But its signature (of course without <S>) is reusable:

interface IStoreEnhancer extends Function {
    (createStore: IStoreCreator): IStoreCreator;
}

here:

function applyMiddleware<S>(...middlewares: IMiddleware<S>[]): IStoreEnhancer;

and here:

interface IStoreCreator extends Function {
    <S, A>(reducer: Reducer<S, A>): Store<S>;
    <S, A>(reducer: Reducer<S, A>, enhancer: IStoreEnhancer): Store<S>;
    <S, A>(reducer: Reducer<S, A>, initialState: S): Store<S>;
    <S, A>(reducer: Reducer<S, A>, initialState: S, enhancer: IStoreEnhancer): Store<S>;
}

Reducer

It can be still a generic type:

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

interface IStoreCreator extends Function {
    <S, A>(reducer: Reducer<S, A>): Store<S>;
   …

so anyone who uses some middleware and need many types of action-like-things can use

const rootReducer = (state: MyState, a: any): MyState => {…};
const myStore = createStore(rootReducer);
@mikekidder

This comment has been minimized.

Copy link
Contributor

commented Feb 17, 2016

Has this PR been compared to what has already been submitted to typings/registry?

Which one takes precedence if someone does "typings search redux" not realizing a definition file exists in Redux repo? I can see it bringing confusion, and in turn, issues opened on Redux repo.

index.d.ts Outdated
@@ -0,0 +1,60 @@
export interface Action {
type: string;

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 17, 2016

Author Collaborator

Is this correct? Do we expect action type to always be string, or should it be any?

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 17, 2016

Maybe

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

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 17, 2016

Author Collaborator

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.

index.d.ts Outdated

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

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

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 17, 2016

Author Collaborator

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

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 17, 2016

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…

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 17, 2016

Author Collaborator

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.

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 17, 2016

Author Collaborator

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.

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 17, 2016

Author Collaborator

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

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 17, 2016

Author Collaborator

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.

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 17, 2016

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

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 17, 2016

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

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 18, 2016

Author Collaborator

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?

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 18, 2016

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;
}
index.d.ts Outdated


export interface ActionCreator {
(...args: any[]): any;

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 17, 2016

Author Collaborator

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

@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Feb 17, 2016

@mikekidder I guess once this PR gets merged it will become official typings for Redux and Typings registry should point to it.

@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Feb 17, 2016

Added some tests using https://github.com/adamcarr/typescript-definition-tester.
For now it requires to add typescript and chai to devDependencies, until this is resolved: adamcarr/typescript-definition-tester#1

index.d.ts Outdated

export interface Middleware {
<S>(api: MiddlewareAPI<S>): (next: Dispatch) => Dispatch;
}

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 17, 2016

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

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 17, 2016

Author Collaborator

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.

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 17, 2016

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

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 17, 2016

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

;)

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 18, 2016

Author Collaborator

The problem is — thunkMiddleware is standalone and it can't have knowledge of what MyStore is.

index.d.ts Outdated

// Minimal typing for more than 4 parameters
export function compose<Result>(f1: (a: any) => Result,
...functions: Function[]): (a: any) => Result;

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 17, 2016

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
@gaearon

This comment has been minimized.

Copy link
Contributor

commented Feb 17, 2016

@aikoven @ulfryk

I added you both to collaborators with write access and leave this one to you 😉 . Please work it out between yourselves how/whether you’d like to collaborate, and merge this when you think it’s good enough. I also trust you to maintain these and will ping you if we introduce any changes to the API.

Thanks!

@ulfryk

This comment has been minimized.

Copy link

commented Feb 17, 2016

@gaearon - Thank you very much for trust :) We'll try to do our best!

Update TypeScript definitions
More strict types: replace `any` with type parameters
Add middleware tests
@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Feb 18, 2016

@ulfryk Updated typings. Please also see comments for outdated diffs.

I have one concern for Action type yet. It's clear that user will have to extend Action to describe shape of actions in their app. So if we have

interface Action {
  type: string;
}

then user can have e.g. FSA like this:

interface MyAction<P> extends Action {
  payload: P;
}

or free-shape action:

interface MyAction extends Action {
  [key: string]: any;
}

If you want to constraint type property:

type ActionType = 'A' | 'B' | 'C';

interface MyAction extends Action {
  type: ActionType;
  // ...
}

Although if ActionType was enum, this wouldn't compile, because String Enums aren't supported yet. We could loosen constraint for type to be any, but I'm not sure if we should: values of enum are integers, which would make it hard to analyze action log with DevTools.

Still Redux docs don't enforce type to be string:

Types should typically be defined as string constants.

cc @gaearon

@ulfryk

This comment has been minimized.

Copy link

commented Feb 18, 2016

@aikoven - about Action interface - we have 2 possibilities:

Strict

interface Action<T> {
    type: T;
}
// So user will always have to specify type of `type` property

then user can have e.g. FSA like this:

interface MyAction<P> extends Action<string> {
  payload: P;
}

// or

interface MyAction<P, T> extends Action<T> {
  payload: P;
}

or free-shape action:

interface MyAction extends Action<string> {
  [key: string]: any;
}

If you want to constraint type property:

type ActionType = 'A' | 'B' | 'C';

interface MyAction extends Action<ActionType> {
  // ...
}

and TS enums will also work:

type ActionType = { A, B, C }

interface MyAction extends Action<ActionType> {
  // ...
}

or

Elastic

interface Action {
    type: any;
}

It works in all your examples plus TS enums, but User always have to define his own interface/type for actions if he want's stricter mode.

Conclusion

IMO stricter version is much better:

interface Action<T> {
    type: T;
}
index.d.ts Outdated

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

export function combineReducers<S>(reducers: {[key: string]: Reducer<any>}): Reducer<S>;

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 18, 2016

I think that reducers map should be defined separately:

export interface ReducersMapObject {
    [key: string]: Reducer<any>;
}

export function combineReducers<S, M extends ReducersMapObject>(reducers: M): Reducer<S>;

So anyone can easily make his own type for this corresponding with his State type if one was defined:

interface MyReducers extends ReducersMapObject {
    'posts': Reducer<Post[]>;
    'user': Reducer<MyUser>;
    … 
}

const reducers: MyReducers = …;

const myRootReducer = combineReducers<Reducer<State>, MyReducers>(reducers);

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 18, 2016

Author Collaborator

I see one drawback here: until TypeScript supports default type parameters we'd have to always specify second parameter which adds verbosity.

How about overloads:

export function combineReducers<S>(reducers: ReducersMapObject): Reducer<S>;
export function combineReducers<S, M extends ReducersMapObject>(reducers: M): Reducer<S>;

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 18, 2016

Overloading makes sense 👍

But have to be well tested ;)

index.d.ts Outdated
export interface Store<S> {
dispatch: Dispatch;
getState(): S;
subscribe(listener: () => void): () => void;

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 18, 2016

One more lil suggestion here - lets create type/interface for function returned from subscribe so user can declare variable which will hold reference to it in future:

// index.d.ts
export interface Unsubscribe extends Function {
    (): void;
}

export interface Store<S> {
    …
    subscribe(listener: () => void): Unsubscribe;
    …
}

// real example from my code:
@Component({selector: 'my-app', templateUrl: '/src/app.html'})
export class App implements OnInit, OnDestroy {
    constructor(@Inject('AppStore') private appStore: Store<IAppState>) {}
    …

    // I can declare it with proper type;
    private unsubscribe: Unsubscribe;

    // and assign value later
    public ngOnInit(): void {
        this.unsubscribe = this.appStore.subscribe(() =>doSomethingWith(this.appStore.getState())… );
    }

    public ngOnDestroy(): void {
        this.unsubscribe();
    }

}

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 18, 2016

Author Collaborator

This makes sense. One question: what's the reasoning behind extends Function? Isn't it redundant when interface already has call signature?

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 18, 2016

I did not test it but i think that Function has few things like bind that are not declared with just call signature… But I may be wrong. It may be redundant anyway.

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 18, 2016

Author Collaborator

I've checked, call signature is enough.

This comment has been minimized.

Copy link
@ulfryk
index.d.ts Outdated
T extends ActionCreator|{[key: string]: ActionCreator}
>(actionCreators: T, dispatch: Dispatch): T;


This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 18, 2016

Action creators:

I think that overloading is much better than union:

// or maybe "<A  extends Action>" but due to middleware issues better without "extends Action"
export interface ActionCreator<A> extends Function {
    (...args: any[]): A;
}

export interface ActionCreatorsMapObject {
    [key: string]: <A>(...args: any[]) => A;
}

export interface ActionCreatorsBinder {
    <A>(actionCreator: ActionCreator<A>, dispatch: Dispatch): ActionCreator<A>;
    <M extends ActionCreatorsMapObject>(actionCreators: M, dispatch: Dispatch): M;
    (actionCreators: ActionCreatorsMapObject, dispatch: Dispatch): ActionCreatorsMapObject;
}

export function bindActionCreators: ActionCreatorsBinder;

I've tested it with such piece of useless code:

const oneCreatorA = bindActionCreators<Action>(a => a, store.dispatch);
const oneCreatorB = bindActionCreators(oneCreatorA, store.dispatch);
const oneCreatorC = bindActionCreators(oneCreatorB, store.dispatch);

interface Ott extends ActionCreatorsMapObject {
    one: IActionCreator<Action>;
    two: IActionCreator<Action>;
    three: IActionCreator<Action>;
    four: IActionCreator<Action>;
}

const xCreatorA = bindActionCreators({
    one: a => a,
    two(b: Action) {return b;},
    three: oneCreatorA
}, store.dispatch);

const cMap: Ott = {
    one: a => a,
    two(b: Action) {return b;},
    three: oneCreatorA,
    four: oneCreatorC
};

const xCreatorB: ActionCreatorsMapObject = bindActionCreators(xCreatorA, store.dispatch);
const xCreatorC: Ott = bindActionCreators(cMap, store.dispatch);

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 18, 2016

Author Collaborator
export interface ActionCreatorsBinder {
    <A>(actionCreator: ActionCreator<A>, dispatch: Dispatch): ActionCreator<A>;
    <M extends ActionCreatorsMapObject>(actionCreators: M, dispatch: Dispatch): M;
    (actionCreators: ActionCreatorsMapObject, dispatch: Dispatch): ActionCreatorsMapObject;
}

Third overload seems redundant here, we can omit type parameter in second overload and it will be inferred as ActionCreatorsMapObject.

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 18, 2016

Author Collaborator

Also consider the difference between

<A>(actionCreator: ActionCreator<A>, dispatch: Dispatch): ActionCreator<A>;

and

<A extends ActionCreator<any>>(actionCreator: A, dispatch: Dispatch): A;

I guess the latter is stronger because let's you constraint not only return type of ActionCreator, but its full signature including argument types.

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 18, 2016

Second one here:

<A extends ActionCreator<any>>(actionCreator: A, dispatch: Dispatch): A;

is not typesafe as it enforces any.

I think we should find better way…

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 18, 2016

@aikoven also you said

Third overload seems redundant here, we can omit type parameter in second overload and it will be inferred as ActionCreatorsMapObject.

And that is great :)
It gives possibility to ignore type parameter if one chooses straight ActionCreatorsMapObject.

This comment has been minimized.

Copy link
@aikoven

aikoven Feb 18, 2016

Author Collaborator
<A extends ActionCreator<any>>(actionCreator: A, dispatch: Dispatch): A;

is not typesafe as it enforces any.

I'm not sure I understand. This signature allows doing

bindActionCreators<ActionCreator<MyAction>>(...)

and even

bindActionCreators<(text: string) => MyAction>(...)

So type parameter is not enforced.

In contrast, if we used this signature:

<A>(actionCreator: ActionCreator<A>, dispatch: Dispatch): ActionCreator<A>;

to bind action creator of type (text: string) => MyAction, then bindActionCreators would return value of type (...args: any[]) => MyAction, effectively erasing constraints on its arguments.

Also, I found that we don't cover cases when bindActionCreators argument and return value have different types, e.g. if action creator returns thunk, then bound action creator will return what thunk returned.

This comment has been minimized.

Copy link
@ulfryk

ulfryk Feb 18, 2016

Yeah, you're right. My mistake:

<A extends ActionCreator<any>>(actionCreator: A, dispatch: Dispatch): A;

Is totally ok.

Also, I found that we don't cover cases when bindActionCreators argument and return value have different types, e.g. if action creator returns thunk, then bound action creator will return what thunk returned.

So maybe third overload:

<A extends ActionCreator<any>, B extends ActionCreator<any>>(
    actionCreator: A, 
    dispatch: Dispatch
): B;
@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Mar 21, 2016

Please note #1526

@Igorbek

This comment has been minimized.

Copy link
Contributor

commented Mar 22, 2016

This too #1537

@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Mar 31, 2016

For everyone concerned with stronger typings for dispatch, please see @Igorbek's proposal: #1537.

It's based on module/interface augmentation introduced in TS 1.8. So to add dispatch signature for e.g. redux-thunk, you would do:

// redux-thunk.d.ts

declare module "redux" {
    export interface Dispatch<S> {
        <R>(asyncAction: (dispatch: Dispatch<S>, getState: () => S) => R): R;
    }
}

This looks like a decent solution to me, but I'd like to hear from others before merging.

@gaearon

This comment has been minimized.

Copy link
Contributor

commented Apr 2, 2016

@aikoven

Sorry I haven’t been maintaining Redux closely for the past couple of months.
What’s up with these? Are we good to cut a minor release with this, or shall we wait for more changes?

@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Apr 4, 2016

@gaearon IMO we should wait at least for #1537.
Also there's a discussion #1526 considering StoreEnhancer/StoreCreator types.

@gaearon gaearon added this to the 4.0 milestone Apr 4, 2016

@Ciantic

This comment has been minimized.

Copy link

commented Apr 4, 2016

Would it be impossible to throw the contents of these: https://github.com/reactjs/redux/blob/master/index.d.ts to inside the declare namespace in here: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/redux/redux.d.ts

I had to do it manually couple of times already.

Currently most people are getting their types from there, at least it would match the official types in upcoming release this way.

@gaearon

This comment has been minimized.

Copy link
Contributor

commented Apr 4, 2016

@aikoven OK. Ping me here or, if I miss it, on Twitter, when you’re ready to cut it.

@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Apr 4, 2016

@Ciantic We'll probably have to copy these typings to DT, because it seems there's no other way to treat other typings in DT that are dependent on Redux.

@Igorbek

This comment has been minimized.

Copy link
Contributor

commented Apr 4, 2016

It makes sense, just to allow seamless transition between these twos. I'll create a PR there and once you make latest redux official I'll merge it.

@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Apr 5, 2016

@gaearon We're good to go

@gaearon

This comment has been minimized.

Copy link
Contributor

commented Apr 5, 2016

@aikoven What’s the latest consensus, is doing this in a minor bump okay?

@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Apr 5, 2016

I've tested for my build: DT typings take precedence over these, so they don't break anything. New Redux typings seem to be ignored unless user takes some steps i.e. manually unlink other dependent libraries' typings from old DT Redux typings so that TS compiler could resolve imports from redux to new ones.

So yes, it is safe to release this as a minor.

@gaearon

This comment has been minimized.

Copy link
Contributor

commented Apr 5, 2016

What if the user had no typings at all? Is this possible in TypeScript?

@Igorbek

This comment has been minimized.

Copy link
Contributor

commented Apr 5, 2016

@gaearon if user hadn't DT typings, the typing shipped with the module would be resolved. That said, a user that just installed 'redux' module can start using TypeScript without need to install DT or other external typings. See Typings in NPM modules in TypeScript Wiki

@gaearon

This comment has been minimized.

Copy link
Contributor

commented Apr 5, 2016

Then this is a breaking change.

  1. User has no typings
  2. We add typings in a minor version
  3. Their build process fails

Is this correct?

@Igorbek

This comment has been minimized.

Copy link
Contributor

commented Apr 5, 2016

I don't think it will fail. If user had no typings it wouldn't be able to use any explicit redux types.

@Igorbek

This comment has been minimized.

Copy link
Contributor

commented Apr 5, 2016

Anyway, I might misunderstand and if it's possible to break a build somehow, it's a breaking change that needs major.

@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Apr 6, 2016

Can we then bump major without waiting until other issues from 4.0 milestone are resolved? There's increasing number of people who want this released asap. How about RC version?

@Igorbek

This comment has been minimized.

Copy link
Contributor

commented Apr 6, 2016

Personally, I'm pretty sure that adding typing bindings is a non-breaking change that theoretically might break build in very synthetic case. Anyway, I'm not insisting.

@Ciantic

This comment has been minimized.

Copy link

commented Apr 6, 2016

  1. User has no typings
  2. We add typings in a minor version
  3. Their build process fails

Then the user is not using TypeScript, because it's not possible to go past point number one on this list.

Because you can't use TypeScript without typings, they get big honking error message if they try to use redux without typings. It just doesn't compile until the error message is handled, and they have to either write any declaration themselves or install them from DT.

Though, most of the people using TS has to deal with wildly changing typings anyway, so if there is some hypothethical case where someone has used redux without typings, and if typings entry appears it suddenly uses TS, it's a weird one.

@ulfryk

This comment has been minimized.

Copy link

commented Apr 6, 2016

I 100% agree with @Ciantic

@aikoven

This comment has been minimized.

Copy link
Collaborator Author

commented Apr 8, 2016

@gaearon I guess the consensus so far is that bumping major is not necessary here.

@gaearon gaearon removed this from the 4.0 milestone Apr 8, 2016

@gaearon

This comment has been minimized.

Copy link
Contributor

commented Apr 8, 2016

Out in 3.4.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.