Skip to content

Revamp TypeScript typing with more type safety #2563

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

Merged
merged 5 commits into from
Oct 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 41 additions & 29 deletions index.d.ts
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,
Expand All @@ -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 */

/**
Expand All @@ -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 = any, A extends Action = Action> = (state: S | undefined, action: A) => S;

/**
* 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 = any, A extends Action = Action> = {
[K in keyof S]: Reducer<S[K], A>;
}

/**
Expand All @@ -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 = Action>(reducers: ReducersMapObject<S, A>): Reducer<S, A>;


/* store */
Expand All @@ -92,9 +97,11 @@ 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 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<D = Action> {
<A extends D>(action: A): A;
}

/**
Expand All @@ -109,9 +116,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 = any, A extends Action = Action, N = never> {
/**
* Dispatches an action. It is the only way to trigger a state change.
*
Expand All @@ -138,7 +147,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<A | N>;

/**
* Reads the state tree managed by the store.
Expand Down Expand Up @@ -182,7 +191,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;
}

/**
Expand All @@ -191,11 +200,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>;
}

/**
Expand All @@ -215,10 +226,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.
Expand Down Expand Up @@ -253,8 +265,8 @@ export const createStore: StoreCreator;

/* middleware */

export interface MiddlewareAPI<S> {
dispatch: Dispatch<S>;
export interface MiddlewareAPI<S = any, D = Action> {
dispatch: Dispatch<D>;
getState(): S;
}

Expand All @@ -268,7 +280,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<D>) => Dispatch<D>;
}

/**
Expand Down Expand Up @@ -317,8 +329,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>;
}

/**
Expand All @@ -340,18 +352,18 @@ 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<A>): C;

export function bindActionCreators<
A extends ActionCreator<any>,
B extends ActionCreator<any>
>(actionCreator: A, dispatch: Dispatch<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<A>): M;

export function bindActionCreators<
M extends ActionCreatorsMapObject,
N extends ActionCreatorsMapObject
M extends ActionCreatorsMapObject<any>,
N extends ActionCreatorsMapObject<any>
>(actionCreators: M, dispatch: Dispatch<any>): N;


Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
"rollup-plugin-replace": "^1.1.1",
"rollup-plugin-uglify": "^1.0.1",
"rxjs": "^5.0.0-beta.6",
"typescript": "^2.1.0",
"typescript": "^2.4.2",
"typescript-definition-tester": "0.0.5"
},
"npmName": "redux",
Expand Down
2 changes: 1 addition & 1 deletion test/typescript/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ const t11: number = compose(stringToNumber, numberToString, stringToNumber,


const funcs = [stringToNumber, numberToString, stringToNumber];
const t12 = compose(...funcs)('bar', 42, true);
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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');
19 changes: 10 additions & 9 deletions test/typescript/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ interface AddTodoAction extends Action {
}


const todosReducer: Reducer<TodosState> = (state: TodosState,
action: Action): TodosState => {
switch (action.type) {
case 'ADD_TODO':
return [...state, (<AddTodoAction>action).text]
default:
return state
const todosReducer: Reducer<TodosState, AddTodoAction> =
(state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.text]
default:
return state
}
}
}

const todosState: TodosState = todosReducer([], {
type: 'ADD_TODO',
Expand Down Expand Up @@ -47,7 +47,8 @@ type RootState = {
counter: CounterState;
}

const rootReducer: Reducer<RootState> = combineReducers({

const rootReducer = combineReducers<RootState, Action | AddTodoAction>({
todos: todosReducer,
counter: counterReducer,
})
Expand Down
2 changes: 1 addition & 1 deletion test/typescript/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const reducer: Reducer<State> = (state: State, action: Action): State => {

/* createStore */

const store: Store<State> = createStore<State>(reducer);
const store: Store<State> = createStore(reducer);

const storeWithPreloadedState: Store<State> = createStore(reducer, {
todos: []
Expand Down