{"id":25098541,"url":"https://github.com/zaboco/typed-machine","last_synced_at":"2025-04-19T12:57:14.722Z","repository":{"id":57679346,"uuid":"158745966","full_name":"zaboco/typed-machine","owner":"zaboco","description":"A strict Finite State Machine, written in TS","archived":false,"fork":false,"pushed_at":"2022-05-14T10:24:54.000Z","size":676,"stargazers_count":23,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-11T05:05:45.781Z","etag":null,"topics":["state-machine","typescript-library"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zaboco.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-11-22T20:09:35.000Z","updated_at":"2024-03-18T07:19:06.000Z","dependencies_parsed_at":"2022-08-24T19:40:18.670Z","dependency_job_id":null,"html_url":"https://github.com/zaboco/typed-machine","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zaboco%2Ftyped-machine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zaboco%2Ftyped-machine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zaboco%2Ftyped-machine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zaboco%2Ftyped-machine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zaboco","download_url":"https://codeload.github.com/zaboco/typed-machine/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249699485,"owners_count":21312407,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["state-machine","typescript-library"],"created_at":"2025-02-07T18:32:00.646Z","updated_at":"2025-04-19T12:57:14.683Z","avatar_url":"https://github.com/zaboco.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Typed Machine\nThis library implements a **strict** model of an event-driven **Finite State Machine**. Its strictness is given by the Typescript type definitions, so it is best used in Typescript applications, to benefit from the type restrictions.\n\n\u003e__WARNING!__ The project is in early stages, so the API might change frequently.\n\n\u003c!-- MarkdownTOC levels=\"1,2,3,4\" --\u003e\n\n- [Getting started](#getting-started)\n- [Tutorial](#tutorial)\n  - [The example - an Editable Item](#the-example---an-editable-item)\n  - [Setup](#setup)\n    - [States](#states)\n    - [Transitions and Messages](#transitions-and-messages)\n    - [Models](#models)\n    - [Message Payloads](#message-payloads)\n    - [Wrapping up - Defining the Template](#wrapping-up---defining-the-template)\n  - [Defining the Machine](#defining-the-machine)\n    - [Choosing a renderer](#choosing-a-renderer)\n    - [The Machine Type](#the-machine-type)\n    - [The transitions](#the-transitions)\n    - [The view](#the-view)\n    - [Putting it all together](#putting-it-all-together)\n  - [Making it reusable](#making-it-reusable)\n    - [Custom initial value](#custom-initial-value)\n    - [Add an onChange Callback](#add-an-onchange-callback)\n    - [Wrapping up - A reusable component](#wrapping-up---a-reusable-component)\n- [API](#api)\n  - [Types](#types)\n    - [`DefineTemplate`](#definetemplate)\n    - [`Machine`](#machine)\n    - [`View`](#view)\n    - [`Transitions`](#transitions)\n  - [Containers](#containers)\n    - [React](#react)\n- [Adapters](#adapters)\n  - [Developing a new adapter](#developing-a-new-adapter)\n- [TODO](#todo)\n- [Contributing](#contributing)\n  - [New Adapters](#new-adapters)\n  - [Other ideas](#other-ideas)\n- [Acknowledgments](#acknowledgments)\n\n\u003c!-- /MarkdownTOC --\u003e\n\n\n## Getting started\n```sh\nnpm i typed-machine\n```\n\n## Tutorial\nThis library uses concepts that are pretty abstract, so it's best to introduce them using a concrete example.\n\n### The example - an Editable Item\nLet's say we have the following use-case: we need an UI item that can be edited in-place. It should look like that:\n\n![Editable item UI](./assets/editable-item-ui.gif)\n\nThis seems like a good candidate for a State Machine, so let's make one!\n\n### Setup\n\n#### States\nAt its core, a **Machine** is described by the set of **state**s the system can be in. In our case these are:\n```ts\ntype EditableState = 'Readonly' | 'Editing';\n```\n\n#### Transitions and Messages\nIn order to make the machine interactive, we need to define transitions between the available states. And, since we want a **strict** machine, only certain transitions are legal from any given state.\nIn our case:\n1. From _Readonly_, pressing \"Edit\" enables editing mode.\n2. From _Editing_, pressing \"Save\" will update the item's value.\n3. From _Editing_, pressing \"Cancel\" discards the changes.\n4. There is also a less obvious one, in _Editing_ mode: when typing on the keyboard, the input value updates.\n\nWe'll need some way to trigger these transitions, and that is through **Messages**, which we'll talk about later on. For now, let's just define our messages: START_EDITING, SAVE, DISCARD, CHANGE_TEXT\n\n#### Models\nWe'll need a place to store the value of the item, as well as the \"draft\" value while editing. The straightforward way of doing that is having a record holding these values:\n```ts\ntype Data = {\n  value: string;\n  draft?: string;\n}\n```\nBut this approach can cause issues, because the `Data` can easily become out of sync with the current machine state: What does it mean for an _Editing_ item to have an undefined `draft`? Or for a _Readonly_ item to have a `draft` value?\n\nBut, since we have a State Machine, we can assign each state a certain piece of data, that we are sure it exists in the given state. We'll call that a **Model**. \n\nFor our example, we'll have: \n```ts\ntype ReadonlyModel = { value: string };\ntype EditingModel = { draft: string };\n```\n\nIt is certainly more clear what data each state can store. \n\nThere is, however, an issue: how do we discard the change when _Editing_? The only data we have is the `draft`, and the original `value` is somewhere else, in the _Readonly_ model, that we can't reach, because we want to keep states decoupled.\nBut the solution is simple: just copy along the original value, when we transition from _Readonly_ to _Editing_.\n\nOne other minor improvement is that we can simplify the `ReadonlyModel` to a simple `string`, instead of a one-key record.\nSo the updated models would look like that:\n```ts\ntype ReadonlyModel = string;\ntype EditingModel = { original: string; draft: string };\n```\n\n#### Message Payloads\nOne last thing we need to provide in order to fully specify our Machine's structure, is whether the **Messages** that trigger transitions also have some attached **payload**, something extra coming from the user. \n\nIn our example, when updating the input, we need to know what's the new value, in order to update the `draft`. So the CHANGE_TEXT message will take an extra `string` as a payload.\n\n\n#### Wrapping up - Defining the Template\nUsing all the setup bits from above, we can come up with an unified definition for what the Machine would look like (we'll call that a **Template**):\n```ts\nimport { DefineTemplate } from 'typed-machine';\n\ntype EditableState = 'Readonly' | 'Editing';\n\ntype EditableTemplate = DefineTemplate\u003c\n  EditableState,\n  {\n    Readonly: {\n      transitionPayloads: {\n        START_EDITING: null;\n      };\n      model: string;\n    };\n    Editing: {\n      transitionPayloads: {\n        CHANGE_TEXT: string;\n        SAVE: null;\n        DISCARD: null;\n      };\n      model: { original: string; draft: string };\n    };\n  }\n\u003e;\n```\nWe specify all the transitions implicitly through `transitionPayloads` (to avoid duplication), so we must give a payload type even to those which have no payload. By convention, that type is `null`;\n\n### Defining the Machine\nNow that we specified the structure of the Machine, we can go ahead and instantiate it.\n\n#### Choosing a renderer\nAt its core, the library keeps the view layer abstract, and it uses **Adapters** to actually render something out. For this example we'll use the **React Adapter** (which is the only one at the moment).\n\n#### The Machine Type\nSince we're using the React Adapter, we'll use the `ReactMachine` type:\n```ts\nimport { ReactMachine } from 'typed-machine/react';\n\ntype EditableMachine = ReactMachine\u003cEditableState, EditableTemplate\u003e;\n```\n\n#### The transitions\nAs we said before, transitions are how the Machine gets to a new States, and updates its model. \n\nIn our case, we'll have something like:\n```ts\nimport { Transitions } from 'typed-machine';\n\nconst readonlyTransitions: Transitions\u003cEditableMachine, 'Readonly'\u003e = {\n  START_EDITING: value =\u003e ['Editing', { draft: value, original: value }]\n}\n\nconst editingTransitions: Transitions\u003cEditableMachine, 'Editing'\u003e = {\n  SAVE: ({ draft, original }) =\u003e ['Readonly', draft],\n  DISCARD: ({ original }) =\u003e ['Readonly', original],\n  CHANGE_TEXT: ({ original }, newDraft) =\u003e ['Editing', { original, draft: newDraft }],\n}\n```\n\nSo, transitions have the following pseudo-signature (not actual TS code, it's just illustrative):\n```ts\n{\n  [m in MessageTypes]: (m: Model, p: Payload) =\u003e [SomeState, ModelForThatState]\n}\n```\nWhere the types are:\n- `MessageTypes` - the available messages that can be dispatched from this State.\n- `Model` - the model associated to the current state.\n- `Payload` - the payload for the current transition - it is the type that was defined in `transitionPayloads` before. Of course, if there is no payload (i.e. it is `null`) it does not have to be specified in the function. This is the case for all the messages above, except `CHANGE_TEXT`, which taxes a `string` value.\n- `SomeState` - one of the States of the Machine;\n- `ModelForThatState` - the model associated with `SomeState`. This is one of the places where we can really see the benefits of defining a **Template**: Typescript is smart enough to correlate each State with its model, so we get type-safe transitions.\n\n#### The view\nNow that we've setup all the wiring, we need to actually show something to the user. For that, we'll use a `view` function for each State, which will display the model associated with that State, and also dispatch messages back to the Machine. Since we're using the React Adapter, each `view` must return a `JSX.Element`.\n\n```ts\nimport { View } from 'typed-machine';\n\nconst readonlyView: View\u003cEditableMachine, 'Readonly'\u003e = (dispatch, model) =\u003e (\n  \u003cdiv\u003e\n    {model}\n    \u003cbutton onClick={() =\u003e dispatch('START_EDITING')}\u003eEdit\u003c/button\u003e\n  \u003c/div\u003e\n)\n\nconst editingView: View\u003cEditableMachine, 'Editing'\u003e = (dispatch, { draft }) =\u003e {\n  return (\n    \u003cdiv\u003e\n      \u003cinput\n        type=\"text\"\n        value={draft}\n        onChange={ev =\u003e { dispatch('CHANGE_TEXT', ev.target.value); }}\n      /\u003e\n      \u003cbutton onClick={() =\u003e dispatch('SAVE')}\u003eSave\u003c/button\u003e\n      \u003cbutton onClick={() =\u003e dispatch('DISCARD')}\u003eCancel\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\nSo, a `view` has the following pseudo-signature:\n```ts\n(\n  dispatch: (mt: MessageType, mp?: MessagePayload) =\u003e void, \n  model: Model\n) =\u003e JSX.Element\n```\nWhat is worth noting is that the each view function gets a different `dispatch`, depending on the State: each `dispatch` can only receive messages that are legal for that state. So, for example, `dispatch('START_EDITING')` in `editingView` would cause a compile error, because \n\n##### A note about the `dispatch` function, for Redux users.\n\u003e As the name suggests, this function is similar to the one in Redux. There are, however, a few differences:\n\u003e - The signature - Machine's dispatch takes the message (i.e. action) type and an optional payload, instead of a `{ type: string, ...payload }` action.\n\u003e - Scope - Machine's dispatch accepts only messages that are legal from the current state, whereas a Redux store can handle any message.\n\n#### Putting it all together\nWe can view a machine as a Directed Graph, with **States** as nodes and **Transitions** as edges. Each state will be described by the information we introduced above:\n- The **model**, with the type previously defined in the **Template**\n- The **transitions** functions\n- The **view** function\n\nThe last piece in the puzzle is the initial State of the Machine. And, with that, we can finally instantiate our `EditableMachine`:\n```ts\nconst editableMachine: EditableMachine = {\n  current: 'Readonly',\n  graph: {\n    Readonly: {\n      model: 'Whatever',\n      transitions: readonlyTransitions,\n      view: readonlyView,\n    },\n    Editing: {\n      model: { draft: '', original: '' },\n      transitions: editingTransitions,\n      view: editingView,\n    },\n  },\n};\n```\n\nAnd, in order to integrate it in the React application, we wrap the machine instance in a `\u003cMachineContainer /\u003e`, provided by the React Adapter:\n```ts\nimport { MachineContainer } from 'typed-machine/react';\n\nconst EditableItem = () =\u003e (\n  \u003cMachineContainer {...editableMachine} /\u003e\n);\n```\n\n### Making it reusable\nNow, we can use as many `\u003cEditableItem /\u003e`s as we like in our app. Each will hold its internal state machine.  \nBut, as it is defined now, it's not that reusable. However, we can easily make some improvements.\n\n#### Custom initial value\nInstead of a hard-coded `editableMachine`, we can use a factory function:\n```ts\nconst makeEditableMachine = (defaultValue: string): EditableMachine =\u003e ({\n  current: 'Readonly',\n  graph: {\n    Readonly: {\n      model: defaultValue,\n      // ... same as before\n    },\n    Editing: {\n      model: { draft: defaultValue, original: defaultValue },\n      // ... same as before\n    },\n  },\n})\n```\n\n#### Add an onChange Callback\nTo integrate our component in a bigger app, we might also need to notify the component's parent that the value has changed (when pressing \"Save\").\nThis feature is actually easy to add, with no special API: just use the callback in the appropriate **transition**:\n```ts\nfunction makeReadonlyTransitions(\n  onChange: (t: string) =\u003e void,\n): Transitions\u003cEditableMachine, 'Editing'\u003e {\n  return {\n    SAVE: ({ draft, original }) =\u003e {\n      if (draft !== original) {\n        onChange(draft);\n      }\n      return ['Readonly', draft];\n    },\n    // DISCARD: ... same as before\n    // CHANGE_TEXT: ... same as before\n  };\n}\n```\n\n#### Wrapping up - A reusable component\n```ts\nexport type EditableItemProps = {\n  defaultValue: string;\n  onChange: (s: string) =\u003e void;\n};\n\nconst makeEditableMachine = ({ defaultValue, onChange }: EditableItemProps): EditableMachine =\u003e ({\n  current: 'Readonly',\n  graph: {\n    Readonly: {\n      model: defaultValue,\n      transitions: readonlyTransitions,\n      view: readonlyView,\n    },\n    Editing: {\n      model: { draft: defaultValue, original: defaultValue },\n      transitions: makeReadonlyTransitions(onChange),\n      view: editingView,\n    },\n  },\n});\n\nexport const EditableItem = (props: EditableItemProps) =\u003e (\n  \u003cMachineContainer {...makeEditableMachine(props)} /\u003e\n);\n```\n\n## API\n\n### Types\n\n#### `DefineTemplate`\nThis is a higher order type, that build the Template for the Machine. It takes the State, and a definition type that must have the following shape:\n```ts\nRecord\u003cstring, {\n  transitionPayloads: Record\u003cstring, any\u003e;\n  model: any;  \n}\u003e\n```\n\nExample:\n```ts\nimport { DefineTemplate } from 'typed-machine';\n\ntype SimpleState = 'Default';\n\ntype SimpleTemplate = DefineTemplate\u003cSimpleState, {\n    Default: {\n      transitionPayloads: {\n        NOOP: null;\n      };\n      model: null;\n    };\n  }\n\u003e;\n```\n#### `Machine`\nThis type is at the core of the library. It's a higher order type, taking the View type, a State and a Template:\n```ts\nimport { Machine } from 'typed-machine';\n\ntype SimpleMachine = Machine\u003cJSX.Element, SimpleState, SimpleTemplate\u003e;\n```\n\nHowever, type aliases provided by Adapters will be used instead:\n```ts\nimport { ReactMachine } from 'typed-machine/react';\n\ntype SimpleMachine = ReactMachine\u003cSimpleState, SimpleTemplate\u003e;\n```\n\n\n#### `View`\nThis is an utility type, which allows extracting `view`s as separate functions:\n```ts\nimport { View } from 'typed-machine';\n\nconst defaultView: View\u003cSimpleMachine, 'Default'\u003e = (dispatch) =\u003e (\n  \u003cbutton onClick={() =\u003e dispatch('NOOP')}\u003eDoing nothing\u003c/button\u003e\n);\n```\n\n#### `Transitions`\nSimilar to `View`, it allows transitions to be defined separately:\n```ts\nimport { Transitions } from 'typed-machine';\n\nconst defaultTransitions: Transitions\u003cSimpleMachine, 'Default'\u003e = {\n  NOOP: () =\u003e ['Default', null],\n};\n```\n\n### Containers\n\n#### React\n```ts\nimport { MachineContainer } from 'typed-machine/react';\n\nconst simpleMachine: SimpleMachine = {\n  current: 'Default',\n  graph: {\n    Default: {\n      model: null,\n      transitions: defaultTransitions,\n      view: defaultView\n    }\n  }\n}\n\nexport const SimpleMachine = () =\u003e (\n  \u003cMachineContainer {...simpleMachine} /\u003e\n);\n```\n\n## Adapters\nIn order to actually do anything useful with a **Machine**, we need an UI implementation, and also some a way to store the State Machine, and update its state once a transition is done. Both of these tasks are accomplished by an **Adapter**. Right now, only a React implementation is provided.\n\n### Developing a new adapter\nThe core library exposes a simple function that allows a parent to 1) render the current State and 2) update external state when the Machine internal representation changes. That function has the following signature:\n\n```ts\nexport function currentView\u003cR, S extends string, GT extends GraphTemplate\u003cS\u003e\u003e(\n  machine: Machine\u003cR, S, GT\u003e,\n  onChange: (updatedMachine: Machine\u003cR, S, GT\u003e) =\u003e void,\n): R\n```\n\nHere, `R` is the return type for the `view` functions.\n\nThere are two things an adapter must expose:\n1. A concrete `Machine` type (basically specifying the `view` return value - `R` from above). For example, the React Adapter exposes:\n```ts\ntype ReactMachine\u003cS extends string, GT extends GraphTemplate\u003cS\u003e\u003e = Machine\u003cJSX.Element, S, GT\u003e\n```\n\n2. Some sort of \"container\" for the Machine. In the React Adapter, it's actually called just that: `MachineContainer` and [its implementation](src/react/MachineContainer.tsx) is really simple. The gist of it is the `render` function:\n```ts\nrender() {\n  return currentView(this.state, newMachine =\u003e {\n    this.setState(newMachine);\n  });\n}\n```\n\n## TODO\nThere are still a lot of features to add, in order to make this library production ready:\n- [ ] Investigate transition guards\n- [ ] Handle side effects, like fetch requests.\n- [ ] Nested Machines - find a good way to scale.\n- [ ] Pluggable addons/middlewares (e.g. History with undo/redo support).\n- [ ] _Nice to have_ Developer tooling (time travel debugging, etc.)\n- [ ] _Nice to have_ React hook.\n- [ ] _Nice to have_ Eslint plugin (e.g. identify unreachable States).\n\n## Contributing\n\nIn case you're interested in this project, and you would like to contribute, there are some things to be done.\n\n### New Adapters\nRight now only the React adapter is provided, but it should be fairly easy to add new ones for other UI libraries such as Vue, Angular, etc. You can read the section about [developing a new adapter](#developing-a-new-adapter) and see [the React implementation](src/react/MachineContainer.tsx) for reference.\n\n### Other ideas\nIf you have other ideas or want to tackle one of the TODOs from above, let's start a discussion. Submit an issue with `[Idea]` of `[Feature]` \"tag\" and we'll start from there.\n\n## Acknowledgments\nThis library borrows from a few sources:\n- The main ideas (and some names) come from the [The Elm Architecture](https://guide.elm-lang.org/architecture/) and the [Elm language](https://elm-lang.org/) in general.\n- Also, Redux was an inspiration (Redux is also [inspired by Elm](https://redux.js.org/introduction/priorart#elm), by the way).\n- The [xstate](https://xstate.js.org/docs/) library served as a reference for implementing Finite State Machines in JavaScript.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzaboco%2Ftyped-machine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzaboco%2Ftyped-machine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzaboco%2Ftyped-machine/lists"}