TypeScript FSA Reducers
TypeScript FSA Reducers provides a fluent, typesafe API for defining Redux reducers. It builds on `typescript-fsa` to streamline reducer creation by removing common boilerplate such as `if-else` chains for action type checking and manual extraction of payloads from actions. Instead, it offers a method-chaining approach with dedicated handlers for specific action creators. The current stable version is 1.2.2. While there isn't a strictly defined release cadence, the library has demonstrated consistent updates and a commitment to stability, notably reaching version 1.0.0. Its key differentiators include the fluent builder pattern (`.case()`, `.default()`, `.withHandling()`, `.build()`), strong type inference for state and payloads, and direct integration with `typescript-fsa`'s action creators, making reducer logic concise and highly maintainable in TypeScript Redux applications.
Common errors
-
TypeError: reducer is not a function
cause Prior to v0.4.0, reducer builder chains were immutable and could be used directly as Redux reducers. After v0.4.0, they became mutable builders, and the final reducer function must be explicitly extracted.fixAlways call `.build()` at the end of your reducer chain to get the actual reducer function: `const myReducer = reducerWithInitialState(...).case(...).build();` -
Cannot find module 'typescript-fsa' or its corresponding type declarations.
cause From v1.0.0, `typescript-fsa` is a peer dependency and must be installed separately by the consumer project. If it's missing, TypeScript will report that the module cannot be found.fixInstall `typescript-fsa` explicitly: `npm install typescript-fsa` or `yarn add typescript-fsa`. -
Argument of type 'undefined' is not assignable to parameter of type 'State'.
cause This error can occur when trying to use a reducer created by `reducerWithoutInitialState()` and passing `undefined` as the state, particularly with `v1.2.2`'s stricter type definitions. `reducerWithoutInitialState` treats `undefined` as a regular state value, not a trigger for initial state.fixIf you intend for `undefined` to trigger an initial state, use `reducerWithInitialState(INITIAL_STATE)`. If you truly want to handle `undefined` as a valid state, ensure your `State` type explicitly includes `| undefined`.
Warnings
- breaking In `v1.0.0`, `typescript-fsa` transitioned from a hard dependency to a peer dependency. If your project relied on `typescript-fsa-reducers` to implicitly install `typescript-fsa`, you must now install `typescript-fsa` explicitly.
- breaking In `v0.4.0`, reducer chains became mutable. Previously, methods like `.case()` returned a new reducer builder instance, but now they modify the existing instance. To obtain an immutable reducer function (which is the standard for Redux), you *must* call the new `.build()` method at the end of your chain.
- gotcha Starting with `v1.2.2`, TypeScript types were updated for `reducerWithoutInitialState`. Passing `undefined` as the *first* argument (representing the state) to reducers created by `reducerWithoutInitialState` will now correctly result in a type error unless `undefined` is explicitly a subtype of your state. This corrects a previous typing oversight.
- gotcha In `v0.4.5`, the type definition of reducers created by builders was updated to explicitly accept `undefined` as its first argument (the state), which causes the initial state to be used (when using `reducerWithInitialState`). While this functionality always existed, the type definitions previously 'hid' it. This change aligns with Redux type definitions.
- gotcha When combining reducers that might have differing or upcasted state types, you may encounter TypeScript errors. The `.build()` method helps to normalize the output type, but complex scenarios might still require careful type management or use of `upcastingReducer`.
Install
-
npm install typescript-fsa-reducers -
yarn add typescript-fsa-reducers -
pnpm add typescript-fsa-reducers
Imports
- reducerWithInitialState
const { reducerWithInitialState } = require('typescript-fsa-reducers');import { reducerWithInitialState } from 'typescript-fsa-reducers'; - reducerWithoutInitialState
import reducerWithoutInitialState from 'typescript-fsa-reducers';
import { reducerWithoutInitialState } from 'typescript-fsa-reducers'; - upcastingReducer
const upcastingReducer = require('typescript-fsa-reducers').upcastingReducer;import { upcastingReducer } from 'typescript-fsa-reducers';
Quickstart
import actionCreatorFactory from 'typescript-fsa';
import { reducerWithInitialState } from 'typescript-fsa-reducers';
const actionCreator = actionCreatorFactory();
interface State {
name: string;
balance: number;
isFrozen: boolean;
}
const INITIAL_STATE: State = {
name: 'Untitled',
balance: 0,
isFrozen: false,
};
const setName = actionCreator<string>('SET_NAME');
const addBalance = actionCreator<number>('ADD_BALANCE');
const setIsFrozen = actionCreator<boolean>('SET_IS_FROZEN');
const myReducer = reducerWithInitialState(INITIAL_STATE)
.case(setName, (state, name) => ({ ...state, name }))
.case(addBalance, (state, amount) => ({
...state,
balance: state.balance + amount,
}))
.case(setIsFrozen, (state, isFrozen) => ({ ...state, isFrozen }))
.default((state, action) => {
// Handle any unhandled actions or simply return the state.
console.log(`Unhandled action: ${action.type}`);
return state;
})
.build(); // Don't forget to call .build() to get the final reducer function
// Example usage (typically with Redux store)
let currentState = myReducer(undefined, { type: '@@INIT' }); // Initial state
console.log('Initial state:', currentState); // { name: 'Untitled', balance: 0, isFrozen: false }
currentState = myReducer(currentState, setName('Alice'));
console.log('After setName:', currentState); // { name: 'Alice', balance: 0, isFrozen: false }
currentState = myReducer(currentState, addBalance(100));
console.log('After addBalance:', currentState); // { name: 'Alice', balance: 100, isFrozen: false }
currentState = myReducer(currentState, { type: 'UNKNOWN_ACTION' });
console.log('After UNKNOWN_ACTION:', currentState); // { name: 'Alice', balance: 100, isFrozen: false } (and console log for unhandled action)