Skip to content

Instantly share code, notes, and snippets.

@tnishimura
Last active December 22, 2019 12:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tnishimura/10041345b5ac71a36a32ecfa81a59980 to your computer and use it in GitHub Desktop.
Save tnishimura/10041345b5ac71a36a32ecfa81a59980 to your computer and use it in GitHub Desktop.
This is a type-safe version of the To-Do list example in Redux's documentation. Everything is typed and should pass in 'strict' mode'. It uses some advanced typescript features such as "keyof".
import { createStore, combineReducers, Reducer } from "redux";
///////////////////////////////////////////////////////////////////////
// action types
// first, define all the action types
const ADD_TODO = "ADD_TODO";
const COMPLETE_TODO = "COMPLETE_TODO";
const REMOVE_TODO = "REMOVE_TODO";
const SET_VISIBILITY_FILTER = "SET_VISIBILITY_FILTER";
const VisibilityFilters = {
SHOW_ALL: "SHOW_ALL",
SHOW_COMPLETED: "SHOW_COMPLETED",
SHOW_ACTIVE: "SHOW_ACTIVE"
} as const;
// we want to create a type that allows "SHOW_ALL", "SHOW_COMPLETED", and "SHOW_ACTIVE".
// We could just do: type IVisibilityFilter "SHOW_ALL" | "SHOW_COMPLETED" | "SHOW_ACTIVE"
// but that is repetitive.
//
// Instead, we can take advantage of two TS keywords, `keyof` and `typeof`.
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html
// typeof VisibilityFilters is something like:
// interface {
// SHOW_ALL: string
// SHOW_COMPLETED: string
// SHOW_ACTIVE: string
// }
//
// so keyof typeof VisibilityFilters === "SHOW_ALL" | "SHOW_COMPLETED" | "SHOW_ACTIVE"
type IVisibilityFilter = keyof typeof VisibilityFilters;
// now, define the interfaces each action payload. the only require element for each is 'type'.
interface IAddTodoAction {
type: typeof ADD_TODO; // can't do "string", if we want proper type detection with switch statements.
id: number;
text: string;
}
interface ICompleteTodoAction {
type: typeof COMPLETE_TODO;
id: number;
}
interface IRemoveTodoAction {
type: typeof REMOVE_TODO;
id: number;
}
interface ISetVisibilityFilterAction {
type: typeof SET_VISIBILITY_FILTER;
visibilityFilter: keyof typeof VisibilityFilters;
}
// we combine all into a single type.
type IAction = IAddTodoAction | ICompleteTodoAction | IRemoveTodoAction | ISetVisibilityFilterAction;
///////////////////////////////////////////////////////////////////////
// action producers
function addTodo(text: string, id: number): IAction {
return {
type: ADD_TODO,
id: id,
text
};
}
function completeTodo(id: number): IAction {
return {
type: COMPLETE_TODO,
id
};
}
function removeTodo(id: number): IAction {
return {
type: REMOVE_TODO,
id
};
}
function setVisibilityFilter(visibilityFilter: IVisibilityFilter): IAction {
return {
type: SET_VISIBILITY_FILTER,
visibilityFilter
};
}
///////////////////////////////////////////////////////////////////////
// state defs
type ITodos = Array<{ text: string; id: number; complete: boolean }>;
interface IState {
todos: ITodos;
visibilityFilter: IVisibilityFilter;
}
///////////////////////////////////////////////////////////////////////
// reducers
const todos: Reducer<ITodos, IAction> = (todos: ITodos = [], action: IAction) => {
switch (action.type) {
case ADD_TODO:
return [...todos, { text: action.text, id: action.id, complete: false }];
case COMPLETE_TODO:
return todos.map(todo => {
if (todo.id === action.id) {
return Object.assign({}, todo, {
complete: true
});
}
return todo;
});
case REMOVE_TODO:
return todos.filter(todo => todo.id !== action.id);
default:
return todos;
}
};
const visibilityFilter: Reducer<IVisibilityFilter, IAction> = (
visibilityFilter: IVisibilityFilter = "SHOW_ALL",
action: IAction
) => {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.visibilityFilter;
default:
return visibilityFilter;
}
};
// combine them
const todoApp = combineReducers<IState>({
todos: todos,
visibilityFilter: visibilityFilter
});
///////////////////////////////////////////////////////////////////////
// use it
const store = createStore(todoApp, {
// initial value. leave createStore's second argument out if you want to use the values from the default values in the reducers
todos: [],
visibilityFilter: "SHOW_COMPLETED"
});
const unsubscribe = store.subscribe(() => console.log(store.getState()));
store.dispatch(addTodo("Test 1", 1));
// { todos: [ { text: 'Test 1', id: 1, complete: false } ],
// visibilityFilter: 'SHOW_COMPLETED' }
store.dispatch(addTodo("Test 2", 2));
// { todos:
// [ { text: 'Test 1', id: 1, complete: false },
// { text: 'Test 2', id: 2, complete: false } ],
// visibilityFilter: 'SHOW_COMPLETED' }
store.dispatch(addTodo("Test 3", 3));
// { todos:
// [ { text: 'Test 1', id: 1, complete: false },
// { text: 'Test 2', id: 2, complete: false },
// { text: 'Test 3', id: 3, complete: false } ],
// visibilityFilter: 'SHOW_COMPLETED' }
store.dispatch(completeTodo(3));
// { todos:
// [ { text: 'Test 1', id: 1, complete: false },
// { text: 'Test 2', id: 2, complete: false },
// { text: 'Test 3', id: 3, complete: true } ],
// visibilityFilter: 'SHOW_COMPLETED' }
store.dispatch(removeTodo(2));
// { todos:
// [ { text: 'Test 1', id: 1, complete: false },
// { text: 'Test 3', id: 3, complete: true } ],
// visibilityFilter: 'SHOW_COMPLETED' }
store.dispatch(setVisibilityFilter('SHOW_ACTIVE'));
// { todos:
// [ { text: 'Test 1', id: 1, complete: false },
// { text: 'Test 3', id: 3, complete: true } ],
// visibilityFilter: 'SHOW_ACTIVE' }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment