
Security News
PHP Foundation Says Chainguard Broke Security Embargo Ahead of Emergency Patches
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.
Type-safe reducers and tagged unions for TypeScript - simplify state management with automatic action creators and pattern matching
Reducerify is a lightweight TypeScript library that simplifies the creation of reducers with full type inference and provides advanced state modeling with tagged unions. It helps you build type-safe reducers, automatically generates action creators, and enables pattern matching for discriminated unions.
matchAll and matchSome# Using pnpm
pnpm install reducerify
# Using bun
bun add reducerify
# Using npm
npm install reducerify
# Using yarn
yarn add reducerify
Immer (optional): For mutable-style reducer updates
pnpm install immer
Zod (optional): Required for tagged unions with runtime validation
pnpm install zod
type TodoState = {
todos: Todo[];
newTodo: Todo
};
type Todo = {
name: string;
isClosed: boolean
};
import { forState, type ActionWithPayload } from "reducerify";
const { reducer, actions } = forState<TodoState>().createReducer({
// Action with payload
updateName: (state, action: ActionWithPayload<{ name: string }>) => {
return {
...state,
newTodo: {
...state.newTodo,
name: action.payload.name,
},
};
},
// Action without payload
save: (state) => {
return {
todos: [...state.todos, state.newTodo],
newTodo: { name: '', isClosed: false },
};
},
// Another action with payload
close: (state, action: ActionWithPayload<{ todoIndex: number }>) => {
return {
...state,
todos: state.todos.map((todo, index) =>
index === action.payload.todoIndex
? { ...todo, isClosed: true }
: todo
),
};
},
});
// Initial state
let state: TodoState = {
todos: [],
newTodo: { name: '', isClosed: false }
};
// Dispatch actions
state = reducer(state, actions.updateName({ name: 'Learn Reducerify' }));
state = reducer(state, actions.save());
state = reducer(state, actions.close({ todoIndex: 0 }));
The Immer integration allows you to write mutable-style updates that are automatically converted to immutable operations.
import { forState, type ActionWithPayload } from "reducerify/reducer/immer";
const { reducer, actions } = forState<TodoState>().createImmerReducer({
updateName: (state, action: ActionWithPayload<{ name: string }>) => {
state.newTodo.name = action.payload.name;
},
save: (state) => {
state.todos.push(state.newTodo);
state.newTodo = { name: '', isClosed: false };
},
close: (state, action: ActionWithPayload<{ todoIndex: number }>) => {
state.todos[action.payload.todoIndex].isClosed = true;
},
});
Tagged unions (also known as discriminated unions or sum types) provide a type-safe way to model data that can be one of several variants. Reducerify's taggedEnum function creates fully typed discriminated unions with built-in pattern matching and validation.
import { taggedEnum } from "reducerify";
import { z } from "zod";
const RemoteData = taggedEnum({
Loading: {},
Success: { data: z.number() },
Failure: { reason: z.string() }
});
const loading = RemoteData.Loading();
const success = RemoteData.Success({ data: 42 });
const failure = RemoteData.Failure({ reason: "Network error" });
type RemoteDataTypes = typeof RemoteData.Types;
type LoadingType = RemoteDataTypes["Loading"];
// { _tag: "Loading" }
type SuccessType = RemoteDataTypes["Success"];
// { _tag: "Success", data: number }
type FailureType = RemoteDataTypes["Failure"];
// { _tag: "Failure", reason: string }
type RemoteDataAll = RemoteDataTypes["All"];
// LoadingType | SuccessType | FailureType
matchAllHandle all possible cases with compile-time exhaustiveness checking:
function handleRemoteData(data: RemoteDataTypes["All"]): string {
return RemoteData.matchAll(data, {
Loading: () => "Loading...",
Success: ({ data }) => `Got: ${data}`,
Failure: ({ reason }) => `Error: ${reason}`
});
}
console.log(handleRemoteData(loading)); // "Loading..."
console.log(handleRemoteData(success)); // "Got: 42"
console.log(handleRemoteData(failure)); // "Error: Network error"
matchSomeHandle only specific cases with an optional default handler:
// With _default handler
const result1 = RemoteData.matchSome(success, {
Success: ({ data }) => `Got: ${data}`,
_default: () => "Other case"
});
// Without _default handler - returns undefined for unhandled cases
const result2 = RemoteData.matchSome(success, {
Success: ({ data }) => `Got: ${data}`
}); // "Got: 42"
const result3 = RemoteData.matchSome(loading, {
Success: ({ data }) => `Got: ${data}`
}); // undefined
Use the is() factory to create type guards for safe type narrowing:
const value: RemoteDataTypes["All"] = getRemoteData();
if (RemoteData.is("Success")(value)) {
// TypeScript knows value is SuccessType
console.log(value.data); // Type-safe access
}
The generated Zod schema enables runtime validation of external data:
// Validate external data
const external = { _tag: "Success", data: 42 };
const validated = RemoteData.schema.parse(external);
// Safe parsing without throwing
const result = RemoteData.schema.safeParse(external);
if (result.success) {
console.log(result.data);
}
import { taggedEnum } from "reducerify";
import { z } from "zod";
const ApiResponse = taggedEnum({
Idle: {},
Loading: {},
Success: {
data: z.object({
id: z.number(),
username: z.string(),
email: z.string().email()
}),
fetchedAt: z.date()
},
Error: {
code: z.enum(["NETWORK_ERROR", "NOT_FOUND", "UNAUTHORIZED"]),
message: z.string()
}
});
type ApiState = typeof ApiResponse.Types.All;
// Usage in a component or function
function renderApiState(state: ApiState): string {
return ApiResponse.matchAll(state, {
Idle: () => "Ready to fetch",
Loading: () => "Fetching data...",
Success: ({ data, fetchedAt }) =>
`User ${data.username} loaded at ${fetchedAt.toISOString()}`,
Error: ({ code, message }) => `[${code}] ${message}`
});
}
// Type-safe filtering
const successStates = states.filter(ApiResponse.is("Success"));
// Handle only errors
const errorMessage = ApiResponse.matchSome(state, {
Error: ({ code, message }) => `${code}: ${message}`
});
You can combine reducers with tagged unions to create powerful state management patterns:
import { forState, taggedEnum, type ActionWithPayload } from "reducerify";
import { z } from "zod";
// Define a tagged union for your state
const LoadingState = taggedEnum({
Idle: {},
Loading: {},
Success: { data: z.array(z.string()) },
Error: { message: z.string() }
});
type AppState = {
items: typeof LoadingState.Types.All;
selectedIndex: number | null;
};
// Create a reducer that works with the tagged union
const { reducer, actions } = forState<AppState>().createReducer({
startLoading: (state) => ({
...state,
items: LoadingState.Loading()
}),
loadSuccess: (state, action: ActionWithPayload<{ data: string[] }>) => ({
...state,
items: LoadingState.Success({ data: action.payload.data })
}),
loadError: (state, action: ActionWithPayload<{ message: string }>) => ({
...state,
items: LoadingState.Error({ message: action.payload.message })
}),
selectItem: (state, action: ActionWithPayload<{ index: number }>) => ({
...state,
selectedIndex: action.payload.index
})
});
// Usage
let state: AppState = {
items: LoadingState.Idle(),
selectedIndex: null
};
state = reducer(state, actions.startLoading());
state = reducer(state, actions.loadSuccess({ data: ["item1", "item2"] }));
// Pattern match on the state
const itemsDisplay = LoadingState.matchAll(state.items, {
Idle: () => "Not loaded yet",
Loading: () => "Loading...",
Success: ({ data }) => `Loaded ${data.length} items`,
Error: ({ message }) => `Error: ${message}`
});
Tagged unions are perfect for implementing type-safe state machines:
const TrafficLight = taggedEnum({
Red: { duration: z.number() },
Yellow: { duration: z.number() },
Green: { duration: z.number() }
});
type TrafficLightState = typeof TrafficLight.Types.All;
function transition(state: TrafficLightState): TrafficLightState {
return TrafficLight.matchAll(state, {
Red: () => TrafficLight.Green({ duration: 30 }),
Yellow: () => TrafficLight.Red({ duration: 45 }),
Green: () => TrafficLight.Yellow({ duration: 5 })
});
}
let light = TrafficLight.Red({ duration: 45 });
light = transition(light); // Green
light = transition(light); // Yellow
light = transition(light); // Red
You can nest tagged unions for complex hierarchical state:
const ServerError = taggedEnum({
NetworkError: { url: z.string() },
TimeoutError: { duration: z.number() },
ValidationError: { fields: z.array(z.string()) }
});
const ApiResult = taggedEnum({
Pending: {},
Success: { data: z.any() },
Failure: { error: z.any() } // Can contain ServerError instances
});
// Usage
const networkError = ServerError.NetworkError({ url: "/api/users" });
const result = ApiResult.Failure({ error: networkError });
// Nested pattern matching
const message = ApiResult.matchAll(result, {
Pending: () => "Waiting...",
Success: ({ data }) => `Got data: ${JSON.stringify(data)}`,
Failure: ({ error }) => {
if (error._tag) {
return ServerError.matchAll(error, {
NetworkError: ({ url }) => `Network error at ${url}`,
TimeoutError: ({ duration }) => `Timeout after ${duration}ms`,
ValidationError: ({ fields }) => `Validation error: ${fields.join(", ")}`
});
}
return "Unknown error";
}
});
The library is organized into two main modules:
src/
├── reducer/
│ ├── pure.ts # Pure functional reducer creation
│ ├── immer.ts # Immer-based reducer creation
│ ├── types.ts # Type definitions for reducers
│ ├── pure.test.ts # Tests for pure reducers
│ └── immer.test.ts # Tests for Immer reducers
├── state/
│ ├── tagged-types.ts # Tagged union implementation
│ └── tagged-types.test.ts # Tests for tagged unions
└── index.ts # Main entry point
Main export (reducerify):
forState, ActionWithPayload, ActionWithoutPayload, etc.taggedEnum, type utilities, etc.Immer export (reducerify/reducer/immer):
forState with createImmerReducerforState<TState>()Creates a reducer factory for a specific state type.
Returns: An object with createReducer method.
createReducer(handlers)Creates a reducer and action creators from handler functions.
Parameters:
handlers: Map of action types to handler functionsReturns:
reducer: The reducer function that handles state updatesactions: Automatically generated action creatorscreateImmerReducer(handlers)Creates a reducer using Immer for mutable-style updates (available from reducerify/reducer/immer).
Parameters:
handlers: Map of action types to handler functions (can mutate state directly)Returns:
reducer: The reducer function that handles state updatesactions: Automatically generated action creatorsActionWithoutPayload: Action without additional dataActionWithPayload<TPayload>: Action with typed payload dataReducerHandlers<TState>: Map of action types to handler functionstaggedEnum<TDefinition>(definition)Creates a tagged enum (discriminated union) with constructors and pattern matching.
Parameters:
definition: Object mapping tag names to their field definitions
{} for tags without payloadReturns: An object containing:
schema: Zod discriminated union schema for validationmatchAll: Exhaustive pattern matching functionmatchSome: Partial pattern matching functionis: Type guard factoryTypes: Type utility for extracting variant typesmatchAll<TReturnType>(value, cases)Performs exhaustive pattern matching on a tagged enum value.
Parameters:
value: The tagged enum instance to matchcases: Object with handlers for all possible tagsReturns: The result of the matching handler
matchSome<TReturnType>(value, cases)Performs partial pattern matching on a tagged enum value.
Parameters:
value: The tagged enum instance to matchcases: Object with handlers for specific tags, plus optional _defaultReturns: The result of the matching handler, or undefined if no handler matches
is<TTag>(tag)Creates a type guard for a specific tag.
Parameters:
tag: The tag name to check forReturns: A type guard function (value) => boolean
TaggedEnumDefinition: Type for the definition objectInferTaggedEnum<TDefinition>: Infers the union type from a definitionInferTaggedTypes<TSchema>: Infers individual variant types from a schemaConstructors<TDefinition>: Type for the constructor functionsMatchAllCases<TDefinition, TReturnType>: Type for exhaustive match casesMatchSomeCases<TDefinition, TReturnType>: Type for partial match casesMIT © Frédéric Mascaro
See LICENSE for more information.
FAQs
Type-safe reducers and tagged unions for TypeScript - simplify state management with automatic action creators and pattern matching
The npm package reducerify receives a total of 0 weekly downloads. As such, reducerify popularity was classified as not popular.
We found that reducerify demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.

Security News
PHP releases emergency patches for two critical RCE vulnerabilities as agencies urge organizations to take unpatched sites offline.

Threat Research
/Security News
A supply chain attack compromised LiteLLM 1.107.0 on PyPI and Docker, spreading malware that steals cloud credentials, developer secrets, and AI provider keys.