Skip to content

Commit

Permalink
Explore pushing the types through Redux all the way
Browse files Browse the repository at this point in the history
How far can we go with autocomplete and compile-time guarantees?

I'm exploring different approaches to doing that with several goals:
 - get full auto-complete on all calls to dispatch, all state values, all action types
 - move runtime overhead into the type system
 - reduce the number of required imports around the app
 - remove existing clutter and minimize syntax required for typing
  • Loading branch information
dmsnell committed Jan 21, 2020
1 parent 29c30c2 commit b3a3952
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 36 deletions.
15 changes: 11 additions & 4 deletions lib/auth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import { reset } from '../state/auth/actions';
import { setWPToken } from '../state/settings/actions';
import { viewExternalUrl } from '../utils/url-utils';

import { MapDispatchToProps } from '../state';

type DispatchProps = {
resetErrors: () => any;
saveWPToken: (token: string) => any;
};

export class Auth extends Component {
static propTypes = {
authorizeUserWithToken: PropTypes.func.isRequired,
Expand Down Expand Up @@ -339,10 +346,10 @@ export class Auth extends Component {
};
}

const mapDispatchToProps = dispatch => ({
resetErrors: () => dispatch(reset()),
saveWPToken: token => dispatch(setWPToken(token)),
});
const mapDispatchToProps: MapDispatchToProps<DispatchProps, {}> = {
resetErrors: reset,
saveWPToken: setWPToken,
};

const mapStateToProps = state => ({
hasInvalidCredentials: hasInvalidCredentials(state),
Expand Down
24 changes: 21 additions & 3 deletions lib/state/action-types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
export const AUTH_SET = 'AUTH_SET';
export const FILTER_NOTES = 'FILTER_NOTES';
export const TAG_DRAWER_TOGGLE = 'TAG_DRAWER_TOGGLE';
import * as T from '../types';

export type Action<
T extends string,
Args extends { [extraProps: string]: any }
> = { type: T } & Args;

export type AuthSet = Action<'AUTH_SET', { status: Symbol }>;
export type FilterNotes = Action<'FILTER_NOTES', { notes: T.NoteEntity[] }>;
export type ToggleTagDrawer = Action<'TAG_DRAWER_TOGGLE', { show: boolean }>;
export type SetFontSize = Action<'setFontSize', { fontSize?: number }>;
export type SetWPToken = Action<'setWPToken', { token: string }>;

export type ActionType =
| AuthSet
| FilterNotes
| ToggleTagDrawer
| SetFontSize
| SetWPToken;

export type Reducer<S> = (state: S, action: ActionType) => S;
2 changes: 2 additions & 0 deletions lib/state/actions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as auth from './auth/actions';
import * as settings from './settings/actions';
import * as ui from './ui/actions';

export default {
auth,
settings,
ui,
};
22 changes: 11 additions & 11 deletions lib/state/auth/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AUTH_SET } from '../action-types';
import * as A from '../action-types';

import {
Authorized,
Expand All @@ -8,27 +8,27 @@ import {
NotAuthorized,
} from './constants';

export const reset = () => ({
type: AUTH_SET,
export const reset = (): A.AuthSet => ({
type: 'AUTH_SET',
status: NotAuthorized,
});

export const setInvalidCredentials = () => ({
type: AUTH_SET,
export const setInvalidCredentials = (): A.AuthSet => ({
type: 'AUTH_SET',
status: InvalidCredentials,
});

export const setLoginError = () => ({
type: AUTH_SET,
export const setLoginError = (): A.AuthSet => ({
type: 'AUTH_SET',
status: LoginError,
});

export const setPending = () => ({
type: AUTH_SET,
export const setPending = (): A.AuthSet => ({
type: 'AUTH_SET',
status: Authorizing,
});

export const setAuthorized = () => ({
type: AUTH_SET,
export const setAuthorized = (): A.AuthSet => ({
type: 'AUTH_SET',
status: Authorized,
});
27 changes: 23 additions & 4 deletions lib/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
* All data should flow through here
*/

import { compose, createStore, combineReducers, applyMiddleware } from 'redux';
import {
applyMiddleware,
combineReducers,
compose,
createStore,
Reducer,
} from 'redux';
import thunk from 'redux-thunk';
import persistState from 'redux-localstorage';
import { omit } from 'lodash';
Expand All @@ -17,6 +23,7 @@ import auth from './auth/reducer';
import settings from './settings/reducer';
import ui from './ui/reducer';

import * as A from './action-types';
import * as T from '../types';

export type AppState = {
Expand Down Expand Up @@ -44,8 +51,8 @@ export type AppState = {
unsyncedNoteIds: T.EntityId[];
};

export const reducers = combineReducers({
appState: appState.reducer.bind(appState),
export const reducers: Reducer<State, A.ActionType> = combineReducers({
appState: appState.reducer.bind(appState) as Reducer<AppState, A.ActionType>,
auth,
settings,
ui,
Expand All @@ -58,7 +65,7 @@ export type State = {
ui: ReturnType<typeof ui>;
};

export const store = createStore(
export const store = createStore<State, A.ActionType, {}, {}>(
reducers,
compose(
persistState('settings', {
Expand All @@ -72,4 +79,16 @@ export const store = createStore(
)
);

export type MapDispatchToPropsFunction<DispatchProps, OwnProps> = (
dispatch: <T extends A.ActionType>(action: T) => T,
ownProps: OwnProps
) => DispatchProps;

export type MapDispatchToProps<
DispatchProps extends {
[P in keyof DispatchProps]: (...args: any[]) => A.ActionType;
},
OwnProps
> = MapDispatchToPropsFunction<DispatchProps, OwnProps> | DispatchProps;

export default store;
5 changes: 3 additions & 2 deletions lib/state/settings/actions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { getIpcRenderer } from '../../utils/electron';
import * as A from '../action-types';

const ipc = getIpcRenderer();

export const setFontSize = fontSize => ({
export const setFontSize = (fontSize?: number): A.SetFontSize => ({
type: 'setFontSize',
fontSize,
});
Expand Down Expand Up @@ -69,7 +70,7 @@ export const setAccountName = accountName => ({
accountName,
});

export const setWPToken = token => ({
export const setWPToken = (token: string) => ({
type: 'setWPToken',
token,
});
Expand Down
11 changes: 6 additions & 5 deletions lib/state/ui/actions.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { FILTER_NOTES, TAG_DRAWER_TOGGLE } from '../action-types';
import * as A from '../action-types';
import * as T from '../../types';

export const filterNotes = notes => ({
type: FILTER_NOTES,
export const filterNotes = (notes: T.NoteEntity[]): A.FilterNotes => ({
type: 'FILTER_NOTES',
notes,
});

export const toggleTagDrawer = show => ({
type: TAG_DRAWER_TOGGLE,
export const toggleTagDrawer = (show: boolean): A.ToggleTagDrawer => ({
type: 'TAG_DRAWER_TOGGLE',
show,
});
17 changes: 10 additions & 7 deletions lib/state/ui/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { difference, union } from 'lodash';
import { combineReducers } from 'redux';
import { FILTER_NOTES, TAG_DRAWER_TOGGLE } from '../action-types';
import * as A from '../action-types';

import * as T from '../../types';

const defaultVisiblePanes = ['editor', 'noteList'];
const emptyList: unknown[] = [];

const filteredNotes = (
const filteredNotes: A.Reducer<T.NoteEntity[]> = (
state = emptyList as T.NoteEntity[],
{ type, notes }: { type: string; notes: T.NoteEntity[] }
) => (FILTER_NOTES === type ? notes : state);
action
) => ('FILTER_NOTES' === action.type ? action.notes : state);

const visiblePanes = (state = defaultVisiblePanes, { type, show }) => {
if (TAG_DRAWER_TOGGLE === type) {
return show
const visiblePanes: A.Reducer<string[]> = (
state = defaultVisiblePanes,
action
) => {
if ('TAG_DRAWER_TOGGLE' === action.type) {
return action.show
? union(state, ['tagDrawer'])
: difference(state, ['tagDrawer']);
}
Expand Down

0 comments on commit b3a3952

Please sign in to comment.