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

Flow types, take 3 (actually 4) #254

Merged
merged 5 commits into from
Jul 19, 2015
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"react/jsx-uses-vars": 2,
"react/react-in-jsx-scope": 2,

// Disable until Flow supports let and const
"no-var": 0,
"vars-on-top": 0,

//Temporarirly disabled due to a possible bug in babel-eslint (todomvc example)
"block-scoped-var": 0,
// Temporarily disabled for test/* until babel/babel-eslint#33 is resolved
Expand Down
9 changes: 9 additions & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[ignore]
.*/lib
.*/test

[include]

[libs]

[options]
26 changes: 17 additions & 9 deletions src/Store.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
/* @flow */

import invariant from 'invariant';
import isPlainObject from './utils/isPlainObject';

import type { State, Action, Reducer } from './types';

export default class Store {
constructor(reducer, initialState) {
state: State;
reducer: Reducer;
listeners: Array<Function>;

constructor(reducer: Reducer, initialState: State): void {
invariant(
typeof reducer === 'function',
'Expected the reducer to be a function.'
Expand All @@ -13,37 +21,37 @@ export default class Store {
this.replaceReducer(reducer);
}

getReducer() {
getReducer(): Reducer {
return this.reducer;
}

replaceReducer(nextReducer) {
replaceReducer(nextReducer: Reducer): void {
this.reducer = nextReducer;
this.dispatch({ type: '@@INIT' });
}

dispatch(action) {
dispatch(action: Action): Action {
invariant(
isPlainObject(action),
'Actions must be plain objects. Use custom middleware for async actions.'
);

const { reducer } = this;
var { reducer } = this;
this.state = reducer(this.state, action);
this.listeners.forEach(listener => listener());
return action;
}

getState() {
getState(): State {
return this.state;
}

subscribe(listener) {
const { listeners } = this;
subscribe(listener: Function): Function {
var { listeners } = this;
listeners.push(listener);

return function unsubscribe() {
const index = listeners.indexOf(listener);
var index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
Expand Down
26 changes: 15 additions & 11 deletions src/createStore.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import Store from './Store';
/* @flow */

import StoreClass from './Store';
import combineReducers from './utils/combineReducers';

import type { State, Reducer, Store } from './types';

export default function createStore(
reducer,
initialState
) {
const finalReducer = typeof reducer === 'function' ?
reducer: Reducer,
initialState: State
): Store {
var finalReducer = typeof reducer === 'function' ?
reducer :
combineReducers(reducer);

const store = new Store(finalReducer, initialState);
var store = new StoreClass(finalReducer, initialState);

return {
dispatch: ::store.dispatch,
subscribe: ::store.subscribe,
getState: ::store.getState,
getReducer: ::store.getReducer,
replaceReducer: ::store.replaceReducer
dispatch: store.dispatch.bind(store),
subscribe: store.subscribe.bind(store),
getState: store.getState.bind(store),
getReducer: store.getReducer.bind(store),
replaceReducer: store.replaceReducer.bind(store)
};
}
11 changes: 9 additions & 2 deletions src/middleware/thunk.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
export default function thunkMiddleware({ dispatch, getState }) {
return next => action =>
/* @flow */

import type { Dispatch, State, Action } from '../types';

type StoreMethods = { dispatch: Dispatch, getState: () => State };

export default function thunkMiddleware(storeMethods: StoreMethods): Dispatch {
var { dispatch, getState } = storeMethods;
return (next: Dispatch) => (action: Action) =>
typeof action === 'function' ?
action(dispatch, getState) :
next(action);
Expand Down
10 changes: 10 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type State = any;
export type Action = Object;
export type IntermediateAction = any;
export type Dispatch = (a: Action | IntermediateAction) => any;
export type Reducer<S, A> = (state: S, action: A) => S;
export type ActionCreator = (...args: any) => Action | IntermediateAction
export type Middleware = (methods: { dispatch: Dispatch, getState: () => State }) => (next: Dispatch) => Dispatch;
export type Store = { dispatch: Dispatch, getState: State, subscribe: Function, getReducer: Reducer, replaceReducer: void };
Copy link
Contributor

Choose a reason for hiding this comment

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

Can/should we clarify subscribe type? The fact that it invokes the listener with no arguments and returns a void function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah we should

export type CreateStore = (reducer: Function, initialState: any) => Store;
export type HigherOrderStore = (next: CreateStore) => CreateStore;
Copy link
Contributor

Choose a reason for hiding this comment

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

Woah. That's pretty awesome.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oops obviously the type of reducer should be Reducer :)

21 changes: 14 additions & 7 deletions src/utils/applyMiddleware.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
/* @flow */

import compose from './compose';
import composeMiddleware from './composeMiddleware';
import thunk from '../middleware/thunk';

/*eslint-disable */
import type { Dispatch, CreateStore, Middleware } from '../types';
/*eslint-enable */

/**
* Creates a higher-order store that applies middleware to a store's dispatch.
* Because middleware is potentially asynchronous, this should be the first
* higher-order store in the composition chain.
* @param {...Function} ...middlewares
* @return {Function} A higher-order store
*/
export default function applyMiddleware(...middlewares) {
const finalMiddlewares = middlewares.length ?
export default function applyMiddleware(
...middlewares: Array<Middleware>
): Dispatch {
var finalMiddlewares = middlewares.length ?
middlewares :
[thunk];

return next => (...args) => {
const store = next(...args);
const middleware = composeMiddleware(...finalMiddlewares);

return (next: CreateStore) => (...args) => {
var store = next(...args);
var middleware = composeMiddleware(...finalMiddlewares);
return {
...store,
dispatch: function dispatch(action) {
const methods = { dispatch, getState: store.getState };
var methods = { dispatch, getState: store.getState };

return compose(
middleware(methods),
Expand Down
8 changes: 7 additions & 1 deletion src/utils/bindActionCreators.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
/* @flow */

import mapValues from '../utils/mapValues';

export default function bindActionCreators(actionCreators, dispatch) {
import type { Dispatch } from '../types';

export default function bindActionCreators(
actionCreators: Object, dispatch: Dispatch
): Object {
return mapValues(actionCreators, actionCreator =>
(...args) => dispatch(actionCreator(...args))
);
Expand Down
8 changes: 5 additions & 3 deletions src/utils/combineReducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import mapValues from '../utils/mapValues';
import pick from '../utils/pick';
import invariant from 'invariant';

function getErrorMessage(key, action) {
import type { Action, State, Reducer } from '../types';

function getErrorMessage(key: String, action: Action): String {
const actionType = action && action.type;
const actionName = actionType && `"${actionType}"` || 'an action';
const reducerName = `Reducer "${key}"`;
Expand All @@ -21,10 +23,10 @@ function getErrorMessage(key, action) {
);
}

export default function combineReducers(reducers) {
export default function combineReducers(reducers: Object): Reducer {
const finalReducers = pick(reducers, (val) => typeof val === 'function');

return function composition(state = {}, action) {
return function composition(state: State = {}, action: Action): State {
return mapValues(finalReducers, (reducer, key) => {
const newState = reducer(state[key], action);
invariant(
Expand Down
4 changes: 3 additions & 1 deletion src/utils/compose.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/* @flow */

/**
* Composes functions from left to right
* @param {...Function} funcs - Functions to compose
* @return {Function}
*/
export default function compose(...funcs) {
export default function compose(...funcs: Array<Function>): Function {
return funcs.reduceRight((composed, f) => f(composed));
}
4 changes: 3 additions & 1 deletion src/utils/createStoreShape.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export default function createStoreShape(PropTypes) {
/* @flow */

export default function createStoreShape(PropTypes: Object): Object {
return PropTypes.shape({
subscribe: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
Expand Down
4 changes: 3 additions & 1 deletion src/utils/identity.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export default function identity(value) {
/* @flow */

export default function identity<T>(value: T): T {
return value;
}
4 changes: 3 additions & 1 deletion src/utils/isPlainObject.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export default function isPlainObject(obj) {
/* @flow */

export default function isPlainObject(obj: Object): boolean {
return obj ? typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.prototype : false;
}
4 changes: 3 additions & 1 deletion src/utils/mapValues.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export default function mapValues(obj, fn) {
/* @flow */

export default function mapValues(obj: Object, fn: Function): Object {
return Object.keys(obj).reduce((result, key) => {
result[key] = fn(obj[key], key);
return result;
Expand Down
4 changes: 3 additions & 1 deletion src/utils/pick.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export default function pick(obj, fn) {
/* @flow */

export default function pick(obj: Object, fn: Function): Object {
return Object.keys(obj).reduce((result, key) => {
if (fn(obj[key])) {
result[key] = obj[key];
Expand Down
12 changes: 7 additions & 5 deletions src/utils/shallowEqual.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
export default function shallowEqual(objA, objB) {
/* @flow */

export default function shallowEqual(objA: Object, objB: Object): boolean {
if (objA === objB) {
return true;
}

const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);

if (keysA.length !== keysB.length) {
return false;
}

// Test for A's keys different from B.
const hasOwn = Object.prototype.hasOwnProperty;
for (let i = 0; i < keysA.length; i++) {
var hasOwn = Object.prototype.hasOwnProperty;
for (var i = 0; i < keysA.length; i++) {
if (!hasOwn.call(objB, keysA[i]) ||
objA[keysA[i]] !== objB[keysA[i]]) {
return false;
Expand Down
16 changes: 9 additions & 7 deletions src/utils/shallowEqualScalar.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export default function shallowEqualScalar(objA, objB) {
/* @flow */

export default function shallowEqualScalar(objA: Object, objB: Object): boolean {
if (objA === objB) {
return true;
}
Expand All @@ -8,22 +10,22 @@ export default function shallowEqualScalar(objA, objB) {
return false;
}

const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);

if (keysA.length !== keysB.length) {
return false;
}

// Test for A's keys different from B.
const hasOwn = Object.prototype.hasOwnProperty;
for (let i = 0; i < keysA.length; i++) {
var hasOwn = Object.prototype.hasOwnProperty;
for (var i = 0; i < keysA.length; i++) {
if (!hasOwn.call(objB, keysA[i])) {
return false;
}

const valA = objA[keysA[i]];
const valB = objB[keysA[i]];
var valA = objA[keysA[i]];
var valB = objB[keysA[i]];

if (valA !== valB || typeof valA === 'object' || typeof valB === 'object') {
return false;
Expand Down