Skip to content

Instantly share code, notes, and snippets.

@japsu
Last active January 2, 2018 17:56
Show Gist options
  • Save japsu/d33f2b210f41de0e24ae47cf8db6a5df to your computer and use it in GitHub Desktop.
Save japsu/d33f2b210f41de0e24ae47cf8db6a5df to your computer and use it in GitHub Desktop.
Type-safe Redux reducers in TypeScript using Immutable.js and typed-immutable-record

Type-safe Redux reducers in TypeScript using Immutable.js and typed-immutable-record

Copyright (C) 2017 Leonidas Oy Ltd
Written by <[email protected]>
Licensed under the MIT license

We attempt to harness the friendly API of Immutable.js to build Redux reducers while trying to preserve as much type safety as possible.

State branches are TypedRecords. A combineReducers that outputs TypedRecords is provided.

Ingredients

References

TODO

  • Reduce boilerplate
  • Typesafe setters
  • Wrap into a library and release into NPM
import { TypedRecord } from 'typed-immutable-record';
interface Reducer<StateType, ActionType> {
(state?: StateType, action?: ActionType): StateType;
}
interface RecordReducer<RecordType extends TypedRecord<RecordType>, ActionType> {
(state?: RecordType, action?: ActionType): RecordType;
}
type RecordReducerMap<PlainType, ActionType> = {
[P in keyof PlainType]: Reducer<PlainType[P], ActionType>;
};
export default function combineReducers<RecordType extends TypedRecord<RecordType>, PlainType, ActionType>(
reducerMap: RecordReducerMap<PlainType, ActionType>,
initialState: RecordType,
): RecordReducer<RecordType, ActionType> {
return function(state: RecordType = initialState, action: ActionType): RecordType {
let newState = state;
for (const key in reducerMap) {
if (reducerMap.hasOwnProperty(key)) {
const reducer = reducerMap[key];
newState = newState.set(key, reducer(newState.get(key), action));
}
}
return newState;
};
}
// Used to denote "any action type not handled by this reducer".
export type OtherAction = { type: '' };
export const OtherAction: OtherAction = { type: '' };
import * as assert from 'assert';
import tasks, { TasksRecord, makeTasks, addTask, setTaskEstimate } from './tasks';
describe('tasks', () => {
const initialState: TasksRecord = makeTasks();
describe('addTask', () => {
it('adds a task', () => {
const title = 'IAmA Task, AMA';
const newState = tasks(initialState, addTask(title));
assert.equal(newState.allTasks.first().title, title);
});
});
describe('setTaskEstimate', () => {
it('sets the estimate of the correct task', () => {
const newState = [
addTask('First task'),
addTask('Second task'),
addTask('Third task'),
setTaskEstimate(1, 5),
].reduce<TasksRecord>(tasks, initialState);
assert.equal(newState.allTasks.get(1).estimate, 5);
});
})
});
import { TypedRecord, makeTypedFactory } from 'typed-immutable-record';
import { List } from 'immutable';
import { OtherAction } from './other';
import combineReducers from './combineReducers';
export type ADD_TASK = 'tasker/tasks/ADD_TASK';
export const ADD_TASK: ADD_TASK = 'tasker/tasks/ADD_TASK';
export type AddTaskAction = {
type: ADD_TASK,
payload: {
title: string,
},
};
export function addTask(title: string): AddTaskAction {
return {
type: ADD_TASK,
payload: { title },
};
}
export type SET_TASK_ESTIMATE = 'tasker/tasks/SET_TASK_ESTIMATE';
export const SET_TASK_ESTIMATE: SET_TASK_ESTIMATE = 'tasker/tasks/SET_TASK_ESTIMATE';
export type SetTaskEstimateAction = {
type: SET_TASK_ESTIMATE,
payload: {
index: number,
estimate: number,
},
};
export function setTaskEstimate(index: number, estimate: number): SetTaskEstimateAction {
return {
type: SET_TASK_ESTIMATE,
payload: { index, estimate },
};
}
export interface Task {
title: string;
estimate?: number;
};
export interface TaskRecord extends TypedRecord<TaskRecord>, Task {}
const makeTask = makeTypedFactory<Task, TaskRecord>({
title: '',
estimate: undefined
});
export interface Tasks {
allTasks: List<TaskRecord>;
}
export interface TasksRecord extends TypedRecord<TasksRecord>, Tasks {}
export const makeTasks = makeTypedFactory<Tasks, TasksRecord>({
allTasks: List<TaskRecord>(),
});
export type TasksAction = AddTaskAction | SetTaskEstimateAction | OtherAction;
function allTasks(
state: List<TaskRecord> = List<TaskRecord>(),
action: TasksAction = OtherAction
): List<TaskRecord> {
switch (action.type) {
case ADD_TASK:
return state.push(makeTask(action.payload));
case SET_TASK_ESTIMATE:
const newTask = state
.get(action.payload.index)
.set('estimate', action.payload.estimate); // TODO typesafe setters
return state.set(action.payload.index, newTask);
default:
return state;
}
}
export default combineReducers<TasksRecord, Tasks, TasksAction>(
{
allTasks,
},
makeTasks()
);
@txomon
Copy link

txomon commented Jun 13, 2017

Hello, did you advance on the creation of this NPM library? I have reached a similar solution with the same packages and I was wondering if this could be an official package =)

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment