Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Lightweight state machines in TypeScript

License

NotificationsYou must be signed in to change notification settings

WinstonFassett/matchina

Repository files navigation

What is Matchina?

Matchina is a TypeScript-first, lightweight toolkit for building type-safe state machines, with powerful pattern matching and async handling. It provides:

  • Strongly-typed state machines with full type inference
  • Tagged unions via thematchbox pattern for discriminated types
  • Type-safe transitions with smart parameter inference
  • Promise integration for handling async operations safely
  • Optional integrations with libraries like Immer, Valibot, Zod, and React

Philosophy & Inspiration

Philosophy & Inspiration

  • Nano-sized, opt-in primitives for state machines and async logic.
  • Inspired byTimsy,XState, and Andre Sitnik’s nano library style.
  • Each primitive is useful standalone or composable.

What is Matchina?

A super lightweight, strongly-typed toolkit for building and extending state machines, factories, and async flows in TypeScript. Use only what you need.

Features

  • Type-Safe State Factories:

    • Create discriminated union types withmatchboxFactory() for powerful pattern matching
    • Build factory machines with complete type inference for states and transitions
    • Enjoy automatic type narrowing with.is() and.as() type guards
  • Smart Transitions:

    • Define transitions with TypeScript inferring parameter types from destination states
    • Trigger transitions with fully typed parameters based on target state requirements
    • Handle complex transition logic with guards and side effects
  • Async Handling:

    • createPromiseMachine for type-safe async state management
    • Lifecycle hooks for promises with appropriate typing
    • Safe error handling for rejected promises
  • React Integration:

    • React hooks for consuming state machines
    • Type-safe component rendering based on state

Installation

npm install matchina

Getting Started

  • Seethe docs site for live examples, guides, and API reference.
  • All examples in the docs are real, runnable code from the repo'sexamples/ directory.

Quick Start

import{matchina,defineStates}from"matchina";// Define states with their associated data typesconststates=defineStates({Idle:undefined,Playing:(trackId:string)=>({ trackId}),Paused:(trackId:string)=>({ trackId}),Stopped:undefined,});constplayer=matchina(states,// Define transitions - TypeScript infers parameter types based on destination states{Idle:{start:"Playing"// Parameter types are automatically matched},Playing:{pause:"Paused",// State transition will preserve trackIdstop:"Stopped"},Paused:{resume:"Playing",// State transition will preserve trackIdstop:"Stopped"},Stopped:{start:"Playing"// Parameter types are automatically matched},},"Idle");// Usage with full type safety:player.start("song-123");// TypeScript knows this needs a trackIdconsole.log(player.getState().key);// "Playing"// TypeScript knows player.getState().data has trackId when in Playing stateif(player.getState().is("Playing")){console.log(player.getState().data.trackId);// "song-123"}player.pause();// TypeScript knows no parameter neededplayer.resume();// TypeScript knows no parameter needed// Pattern matching with exhaustive checkingconstmessage=player.getState().match({Playing:({ trackId})=>`Now playing:${trackId}`,Paused:({ trackId})=>`Paused:${trackId}`,Idle:()=>"Ready to play",Stopped:()=>"Playback stopped",});

Promise Machines

Type-safe state machines for managing asynchronous operations:

import{createPromiseMachine,setup,effect,guard,enter,leave,}from"matchina";// Create a promise machine for async addition with full type inferenceconstadder=createPromiseMachine((a:number,b:number)=>newPromise<number>((resolve)=>setTimeout(()=>resolve(a+b),500)),);// All hooks are strongly typed and TypeScript checkedsetup(adder)(// Type-safe guard to validate parametersguard((ev)=>{// TypeScript knows ev.params exists and contains our parametersif(ev.type!=="executing")returntrue;const[a,b]=ev.params[1];returna>=0&&b>=0;// Only allow non-negative numbers}),// Type-safe hooks for state transitionsenter((ev)=>{if(ev.to.is("Pending")){// TypeScript knows ev.to.data has promise and params when in Pending stateconsole.log("Started addition:",ev.to.data.params);}}),// Log when leaving pending stateleave((ev)=>{if(ev.from.is("Pending")){console.log("Leaving pending state");}}),// Log when promise resolveseffect((ev)=>{if(ev.type==="resolveExit"){// TypeScript knows ev.to.data contains the result when in Resolved stateconsole.log("Promise resolved with:",ev.to.data);}}),);// --- Usage with type safety ---// Execute with properly typed parametersconstdone=adder.execute(2,3);awaitdone;// TypeScript knows this is Promise<number>// Pattern match on state for messagingconstmessage=adder.getState().match({Idle:()=>"Ready to add.",Pending:(params)=>`Adding:${params.params.join(" + ")}`,Resolved:(result)=>`Result:${result}`,Rejected:(error)=>`Error:${error.message}`,});

Core Concepts

Matchbox: Type-Safe Tagged Unions

The foundation of Matchina is thematchbox pattern for creating type-safe tagged unions:

import{matchbox}from"matchina";// Create a state factory with typed data for each stateconstPlayerState=matchbox({Idle:()=>({}),Playing:(trackId:string,startTime:number=Date.now())=>({    trackId,    startTime,}),Paused:(trackId:string,position:number)=>({    trackId,    position,}),Stopped:()=>({}),});// Type-safe state creation:constplayingState=PlayerState.Playing("track-123");constpausedState=PlayerState.Paused("track-123",45);// Exhaustive pattern matching with access to state data:constmessage=playingState.match({Playing:({ trackId, startTime})=>`Now playing:${trackId} (started at${newDate(startTime).toLocaleTimeString()})`,Paused:({ trackId, position})=>`Paused:${trackId} at${position} seconds`,Idle:()=>"Ready to play",Stopped:()=>"Playback stopped",});// Type-safe guards with type narrowing:if(playingState.is("Playing")){// TypeScript knows playingState.data has trackId and startTimeconsole.log(`Playing${playingState.data.trackId} since${playingState.data.startTime}`,);}// Type-safe casting:try{constplaying=pausedState.as("Playing");// Will throw at runtimeconsole.log(playing.data.startTime);// Wouldn't reach this line}catch(e){console.error("Can't cast Paused state to Playing");}

ThematchboxFactory() function provides the foundation for Matchina's state machines, offering:

  • Type-safe state creation with proper parameter inference
  • Exhaustive pattern matching for handling all possible states
  • Type guards that narrow types for precise type checking
  • Type-safe casting for advanced use cases

Factory Machines: Type-Safe State Transitions

Factory Machines combine state factories with transitions for complete type safety:

import{defineStates,createMachine,setup,guard,enter}from"matchina";// Define your state factory with proper typesconstTaskStates=defineStates({Idle:()=>({}),Loading:(query:string)=>({ query}),Success:(query:string,results:string[])=>({ query, results}),Error:(query:string,message:string)=>({ query, message}),});// Create a factory machine with type-safe transitionsconsttaskMachine=createMachine(TaskStates,{Idle:{// Simple string transition - parameter types are inferredsearch:"Loading"},Loading:{// Need functions here because we're adding new datasuccess:(results:string[])=>TaskStates.Success(results),error:(message:string)=>TaskStates.Error(message)},Success:{// Return to idle stateclear:"Idle",// Start a new searchsearch:"Loading"},Error:{// Return to idle stateclear:"Idle",// Retry the same queryretry:({ from})=>TaskStates.Loading(from.data.query)},},"Idle",);// Add hooks with proper typingsetup(taskMachine)(// Guard to prevent empty searchesguard((ev)=>{if(ev.type!=="search")returntrue;returnev.params[0].length>0;}),// Log state transitionsenter((ev)=>{console.log(`Entering${ev.to.key} state`,ev.to.data);}),);// Usage with full type safetytaskMachine.send("search","typescript");// When in Loading state, we can send success with resultsif(taskMachine.getState().is("Loading")){// Simulate API responsesetTimeout(()=>{taskMachine.send("success",["result1","result2"]);},1000);}// Pattern match on current state for UI renderingconstui=taskMachine.getState().match({Idle:()=>"Enter a search term",Loading:({ query})=>`Searching for "${query}"...`,Success:({ query, results})=>`Found${results.length} results for "${query}":${results.join(", ")}`,Error:({ query, message})=>`Error searching for "${query}":${message}`,});

The Factory Machine provides:

  • Type-safe state transitions with parameter inference
  • State data preservation when transitioning between states
  • Lifecycle hooks for intercepting and reacting to state changes
  • Pattern matching for exhaustive state handling

Lifecycle Hooks & Effects

Matchina provides powerful hooks for intercepting and reacting to state transitions:

import{matchina,setup,guard,enter,leave,effect,bindEffects,}from"matchina";// Create a counter machine with effectsconstcounter=matchina(defineStates({Idle:()=>({count:0}),Counting:(count:number)=>({ count}),// Define an effect state that has a specific effect typeMilestone:(count:number)=>({ count,effect:"Notify"asconst}),}),{Idle:{start:"Counting",},Counting:{increment:({ from})=>({key:"Counting",data:{count:from.data.count+1},}),reset:"Idle",},Milestone:{acknowledge:"Counting",},},"Idle",);// Add lifecycle hooks with type safetysetup(counter)(// Guard - prevent incrementing past 100guard((ev)=>{if(ev.type!=="increment")returntrue;if(!ev.from.is("Counting"))returntrue;returnev.from.data.count<100;}),// Enter hook - log entering Milestone stateenter((ev)=>{if(ev.to.is("Milestone")){console.log(`Milestone reached:${ev.to.data.count}`);}}),// Leave hook - log leaving statesleave((ev)=>{console.log(`Leaving${ev.from.key} state`);}),// Effect handler - handle the Notify effectbindEffects({Notify:({ data})=>{// Show notificationalert(`Milestone reached:${data.count}`);},}),);// Usagecounter.start();counter.increment();// count: 1counter.increment();// count: 2// When count reaches 10, Milestone effect will trigger

Lifecycle hooks provide:

  • Guards to prevent invalid transitions
  • Enter/Leave hooks to react to state changes
  • Effect handlers for handling side effects
  • Type-safe API that enforces correct usage

React Integration

Matchina provides integration with React components:

importReactfrom"react";import{matchina}from"matchina";import{onLifecycle}from"matchina";// Create a todo machineconststates=defineStates({Empty:()=>({}),Active:(todos:string[])=>({ todos}),Saving:(todos:string[])=>({ todos}),Error:(message:string)=>({ message}),});consttodoMachine=matchina(states,{Empty:{// Need a function here because we're creating a new arrayadd:(todo:string)=>states.Active([todo])},Active:{// Need a function here to modify existing dataadd:(todo:string)=>({ from})=>states.Active([...from.data.todos,todo]),// Need a function here to modify existing dataremove:(index:number)=>({ from})=>states.Active(from.data.todos.filter((_,i)=>i!==index)),save:"Saving"// Parameters automatically preserved},Saving:{success:"Active",// Parameters automatically preservederror:(message:string)=>states.Error(message)},Error:{dismiss:({ from})=>from?.is("Saving") ?"Active" :"Empty"},},"Empty");// Todo component using the machineconstTodoApp:React.FC=()=>{const[newTodo,setNewTodo]=React.useState("");constmachine=todoMachine;// Register lifecycle hook for the Saving stateonLifecycle(machine,{Saving:{enter:({ to})=>{// Simulate API callsetTimeout(()=>{if(Math.random()>0.8){machine.error("Failed to save todos");}else{machine.success();}},1000);}}});return(<div><h1>Todo App</h1><formonSubmit={(e)=>{e.preventDefault();if(newTodo.trim()){machine.add(newTodo);setNewTodo("");}}}><inputvalue={newTodo}onChange={(e)=>setNewTodo(e.target.value)}placeholder="New todo"/><buttontype="submit">Add</button></form>{machine.getState().match({Empty:()=><p>No todos yet. Add one to get started!</p>,Active:({ todos})=>(<><ul>{todos.map((todo,index)=>(<likey={index}>{todo}<buttononClick={()=>machine.remove(index)}>Remove</button></li>))}</ul><buttononClick={machine.save}>Save</button></>),Saving:()=><p>Saving todos...</p>,Error:({ message})=>(<divclassName="error"><p>Error:{message}</p><buttononClick={machine.dismiss}>Dismiss</button></div>),})}</div>);};

The React integration provides:

  • Type-safe event handlers that match your machine's API
  • Pattern matching for rendering based on machine state
  • Lifecycle hooks for reacting to state changes

Installation

npm install matchina

Documentation

For detailed documentation, examples, and API reference, visit:

Integrations

Matchina is designed to be modular and agnostic of any specific libraries. It can be integrated with various tools as needed.

Store Machine

Matchina's store machine provides a simple way to manage state with transitions:

import{createStoreMachine}from'matchina';// Example with proper type checkingconststore=createStoreMachine(0,{increment:(amt:number=1)=>(change)=>change.from+amt,decrement:(amt:number=1)=>(change)=>change.from-amt,set:(next:number)=>next,reset:()=>0,});// These calls are now properly type-checked:store.dispatch("increment");// Works with default parameterstore.dispatch("increment",5);// Works with explicit parameterstore.dispatch("decrement");// Works with default parameterstore.dispatch("set",42);// Requires a number parameterstore.dispatch("reset");// No parameters requiredconsole.log(store.getState());// 0

Immer Integration

You can use Immer with matchina's store machine for immutable state updates. Here are two patterns:

import{produce}from'immer';import{createStoreMachine}from'matchina';// Pattern 1: Direct transitions with ImmerconstwithImmer=<T>(fn:(draft:T)=>void)=>{return(state:T):T=>produce(state,fn);};// UsageconstinitialState={user:{name:"Alice",preferences:{notifications:true}}};conststore=createStoreMachine(initialState,{updateUserName:(name:string)=>withImmer((draft)=>{draft.user.name=name;})});// Pattern 2: Curried transitions with Immerconststore2=createStoreMachine(initialState,{// For transitions that need access to the change objecttoggleNotifications:()=>(change)=>{returnproduce(change.from,(draft)=>{draft.user.preferences.notifications=!draft.user.preferences.notifications;});}});

Valibot/Zod Integration

You can integrate validation libraries like Valibot or Zod with matchina:

Transition Hooks

Matchina provides several ways to hook into state transitions:

import{matchina,defineStates,setup,transitionHooks,onLifecycle,guard,enter,leave}from'matchina';conststates=defineStates(stateDefinitions);constmachine=matchina(states,transitions,states.InitialState());// Method 1: Using setup with individual hookssetup(machine)(guard((ev)=>{// Prevent invalid transitionsreturntrue;}),enter((ev)=>{console.log(`Entering${ev.to.key} state`);}),leave((ev)=>{console.log(`Leaving${ev.from.key} state`);}));// Method 2: Using transitionHookssetup(machine)(transitionHooks({guard:(ev)=>true},{enter:(ev)=>console.log(`Entering${ev.to.key} state`)},{leave:(ev)=>console.log(`Leaving${ev.from.key} state`)}));// Method 3: Using onLifecycleonLifecycle(machine,{'*':{// For any stateenter:(ev)=>console.log(`Entering${ev.to.key} state`),leave:(ev)=>console.log(`Leaving${ev.from.key} state`),on:{'*':{// For any eventguard:(ev)=>true}}},SpecificState:{enter:(ev)=>console.log('Entering specific state'),on:{specificEvent:(ev)=>console.log('Handling specific event')}}});// For promise machines, you can use guardExecute to prevent starting the promisesetup(promiseMachine)(guardExecute((ev)=>{if(ev.type==='execute'){// Validate parameters before executingconst[value]=ev.params;returnvalue>0;// Only allow positive values}returntrue;// Allow other transitions}));

FAQ

How does Matchina compare to other state machine libraries?

Matchina focuses on TypeScript type inference and composable, lightweight primitives. Unlike XState, it doesn't use a declarative JSON-based configuration. Instead, it provides functional APIs with strong typing. Compared to Timsy, it offers more flexible state transitions and enhanced pattern matching via Matchbox.

Do I need to use the entire library?

No. Matchina is designed to be modular. You can use only the parts you need:

  • UsematchboxFactory alone for type-safe tagged unions
  • UsecreateMachine for state machines
  • UsecreatePromiseMachine for async operations
  • Use React integrations if you're working with React

How small is the library?

Matchina is very lightweight:

FeatureSize (min+gz)
matchbox381 B
factory-machine618 B
promise-machine1.18 kB
react integration397 B
zod integration437 B
valibot integration680 B
full library3.42 kB

Contributing

Contributions are welcome! Feel free to:

  • Report issues and bugs
  • Suggest new features or improvements
  • Submit pull requests

License

MIT

About

Lightweight state machines in TypeScript

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp