Last active
December 22, 2019 12:06
-
-
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".
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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