-
-
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
Conversation
Do we want somebody else to review these too, or is this good to merge? |
for example
does not give strict rules of use, look at this instead:
etc. etc. Of course I'm not talking about naming convention (ignore |
If we can add tests for these, we should add tests. |
|
||
export type Reducer<S> = (state: S, action: any) => S; | ||
|
||
export type Dispatch = (action: any) => 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.
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> {}
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 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.
@ulfryk As far as I can see from return enhancer(createStore)(reducer, initialState) But you are right about original You are also right about type constraint on @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. |
Enhanced creatorYou're right -
here:
and here:
ReducerIt can be still a generic type:
so anyone who uses some middleware and need many types of action-like-things can use
|
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. |
@@ -0,0 +1,60 @@ | |||
export interface Action { | |||
type: string; |
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 be 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.
Maybe
export interface Action<T> {
type: T;
}
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
:
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.
@mikekidder I guess once this PR gets merged it will become official typings for Redux and Typings registry should point to it. |
Added some tests using https://github.com/adamcarr/typescript-definition-tester. |
|
||
export interface Middleware { | ||
<S>(api: MiddlewareAPI<S>): (next: 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.
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… ;)
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 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.
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 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 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)
}
;)
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 problem is — thunkMiddleware
is standalone and it can't have knowledge of what MyStore
is.
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! |
@gaearon - Thank you very much for trust :) We'll try to do our best! |
More strict types: replace `any` with type parameters Add middleware tests
@ulfryk Updated typings. Please also see comments for outdated diffs. I have one concern for 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 ActionType = 'A' | 'B' | 'C';
interface MyAction extends Action {
type: ActionType;
// ...
} Although if Still Redux docs don't enforce
cc @gaearon |
@aikoven - about Action interface - we have 2 possibilities: Strictinterface 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 Elasticinterface 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. ConclusionIMO stricter version is much better: interface Action<T> {
type: T;
} |
|
||
export type Reducer<S> = <A extends Action>(state: S, action: A) => S; | ||
|
||
export function combineReducers<S>(reducers: {[key: string]: Reducer<any>}): Reducer<S>; |
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 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);
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 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>;
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.
Overloading makes sense 👍
But have to be well tested ;)
Please note #1526 |
This too #1537 |
For everyone concerned with stronger typings for It's based on module/interface augmentation introduced in TS 1.8. So to add // 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. |
Sorry I haven’t been maintaining Redux closely for the past couple of months. |
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. |
@aikoven OK. Ping me here or, if I miss it, on Twitter, when you’re ready to cut it. |
@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. |
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. |
@gaearon We're good to go |
@aikoven What’s the latest consensus, is doing this in a minor bump okay? |
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 So yes, it is safe to release this as a minor. |
What if the user had no typings at all? Is this possible in TypeScript? |
@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 |
Then this is a breaking change.
Is this correct? |
I don't think it will fail. If user had no typings it wouldn't be able to use any explicit redux types. |
Anyway, I might misunderstand and if it's possible to break a build somehow, it's a breaking change that needs major. |
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? |
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. |
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. |
I 100% agree with @Ciantic |
@gaearon I guess the consensus so far is that bumping major is not necessary here. |
Out in 3.4.0. |
Original issue: #1401