{"id":13726967,"url":"https://github.com/tableau/ts-checked-fsm","last_synced_at":"2025-12-30T00:42:16.322Z","repository":{"id":37015166,"uuid":"164955240","full_name":"tableau/ts-checked-fsm","owner":"tableau","description":"TypeScript library providing compile-time checking for state machine transitions","archived":false,"fork":false,"pushed_at":"2023-07-13T14:03:29.000Z","size":456,"stargazers_count":95,"open_issues_count":6,"forks_count":15,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-11-14T17:47:50.431Z","etag":null,"topics":[],"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/tableau.png","metadata":{"files":{"readme":"Readme.md","changelog":"Changes.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-01-09T23:36:27.000Z","updated_at":"2024-10-11T03:21:31.000Z","dependencies_parsed_at":"2024-01-07T21:05:07.626Z","dependency_job_id":"5785386b-3935-4760-a724-1ec11b2740f8","html_url":"https://github.com/tableau/ts-checked-fsm","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tableau%2Fts-checked-fsm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tableau%2Fts-checked-fsm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tableau%2Fts-checked-fsm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tableau%2Fts-checked-fsm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tableau","download_url":"https://codeload.github.com/tableau/ts-checked-fsm/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252965120,"owners_count":21832827,"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":[],"created_at":"2024-08-03T01:03:33.583Z","updated_at":"2025-12-30T00:42:16.275Z","avatar_url":"https://github.com/tableau.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# ts-checked-fsm\n\n![Community Supported](https://img.shields.io/badge/Support%20Level-Community%20Supported-457387.svg)\n\nts-checked-fsm provides compile tile validation of state machine transitions leveraging Typescript's powerful type system.\n\n* [Overview](#overview)\n    * [Example](#example)\n    * [Notes](#notes)\n* [Get Started](#get-started)\n    * [Requirements](#requirements)\n    * [Installation](#installation)\n    * [Building](#building)\n* [Contributions](#contributions)\n\n# Overview\nThis library provides a builder pattern API for you declare a finite state machine as a set of states, actions, and transitions, and action handlers. The API is somewhat comparable to other state machine libraries, like XState with one major difference: ts-checked-fsm validates that your state machine is internally consistent and will fail compilation if not.\n\nExamples of things that fail to compile:\n  * You declare transitions between non-existent states\n  * You declare the same state more than once\n  * You declare a handler for the same state and action more than once\n  * You declare a handler for a state or action that doesn't exist\n  * A handler returns type that doesn't match a declared state\n  * A handler for state c returns a state n for which there is no transition from c to n\n  * You forget a handler for a any non-terminal state\n\nThe library uses Error branding and intentionally causes failed type assignments to give you quasi-human-readable error messages. There is a ton of type system devil-magic going on here to make all of this happen.\n\n## Example\n```ts\n  type MoneyPayload = {\n      moneyInserted: number,\n  };\n\n  type ChangePayload = {\n      changeRemaining: number,\n  };\n\n  type InsertMoneyActionPayload = {\n      money: number,\n  };\n\n  const { nextState } = stateMachine()\n      .state('idle')\n      .state\u003c'get-money', MoneyPayload\u003e('get-money')\n      .state\u003c'vend', ChangePayload\u003e('vend')\n      .state\u003c'dispense-change', ChangePayload\u003e('dispense-change')\n      .transition('idle', 'get-money')\n      .transition('get-money', 'get-money')\n      .transition('get-money', 'vend')\n      .transition('vend', 'dispense-change')\n      .transition('dispense-change', 'dispense-change')\n      .transition('dispense-change', 'idle')\n      .action\u003c'insert-money', InsertMoneyActionPayload\u003e('insert-money')\n      .action\u003c'vend-soda'\u003e('vend-soda')\n      .action\u003c'clock-tick'\u003e('clock-tick')\n      .actionHandler('idle', 'insert-money', (_c, a) =\u003e {\n          return {\n              stateName: 'get-money',\n              moneyInserted: a.money,\n          } as const;\n      })\n      .actionHandler('get-money', 'insert-money', (c, a) =\u003e {\n          return {\n              stateName: 'get-money',\n              moneyInserted: c.moneyInserted + a.money\n          } as const;\n      })\n      .actionHandler('get-money', 'vend-soda', (c, _a) =\u003e {\n          return c.moneyInserted \u003e= 50 ? {\n              stateName: 'vend',\n              changeRemaining: c.moneyInserted - 50\n          } as const : c;\n      })\n      .actionHandler('vend', 'clock-tick', (c, _a) =\u003e {\n          return {\n              stateName: 'dispense-change',\n              changeRemaining: c.changeRemaining\n          } as const;\n      })\n      .actionHandler('dispense-change', 'clock-tick', (c, _a) =\u003e {\n          const coinVal = c.changeRemaining \u003e= 25\n              ? 25\n              : c.changeRemaining \u003e= 10\n              ? 10\n              : c.changeRemaining \u003e= 5\n              ? 5\n              : 1;\n\n          return c.changeRemaining - coinVal \u003e 0 ? {\n              stateName: 'dispense-change',\n              changeRemaining: c.changeRemaining - coinVal\n          } as const : {\n              stateName: 'idle'\n          } as const;\n      })\n      .done();\n\n      let n = nextState({stateName: 'idle'}, { actionName: 'clock-tick'});\n      // Idle state doesn't repsond to clock-tick, so state is unchanged\n      expect(n).toEqual({stateName: 'idle'});\n      n = nextState({stateName: 'idle'}, { actionName: 'insert-money', money: 25})\n      expect(n).toEqual({stateName: 'get-money', moneyInserted: 25});\n      n = nextState(n, { actionName: 'insert-money', money: 25});\n      expect(n).toEqual({stateName: 'get-money', moneyInserted: 50});\n      n = nextState(n, { actionName: 'insert-money', money: 27});\n      expect(n).toEqual({stateName: 'get-money', moneyInserted: 77});\n      n = nextState(n, { actionName: 'vend-soda'});\n      expect(n).toEqual({stateName: 'vend', changeRemaining: 27});\n      n = nextState(n, { actionName: 'clock-tick'});\n      expect(n).toEqual({stateName: 'dispense-change', changeRemaining: 27});\n      n = nextState(n, { actionName: 'clock-tick'});\n      expect(n).toEqual({stateName: 'dispense-change', changeRemaining: 2});\n      n = nextState(n, { actionName: 'clock-tick'});\n      expect(n).toEqual({stateName: 'dispense-change', changeRemaining: 1});\n      n = nextState(n, { actionName: 'clock-tick'});\n      expect(n).toEqual({stateName: 'idle'});\n```\n\n## Notes\n* Self transition are not implicit. You must explicitly declare them if a handler for state `x` is allowed to return state `x`.\n* You don't have to declare handlers for final states (i.e. those that have no transitions out of them). If fact, it's illegal to do so since they have no valid transitions out of them!\n* As shown in the example, states and actions can have a payload.\n* For handlers that can return multiple state types depending on some condition, every state must be a legal transition.\n\n## How it works\nThis library uses clever constructions using Typescript's type system. More details in this blog [post](https://engineering.tableau.com/really-advanced-typescript-types-c590eee59a12).\n\n# Get started\n\n## Requirements:\nTypescript 4.0+ or equivalent bundler loader (e.g. ts-loader for webpack).\n\n## Installation\nAdd ts-checked-fsm as a dependency in your package.json.\n\n## Building\n\n### Setup\nBefore any of the following tasks, you need to install dependencies:\n```\nyarn install\n```\n\nWhile untested, you can probably substitute npm for yarn and things will probably work.\n\n### Compilation\n```\nyarn run build\n```\n\nOutput appears in lib folder\n\n# Contributions\nCode contributions and improvements by the community are welcomed!\nSee the LICENSE file for current open-source licensing and use information.\n\nBefore we can accept pull requests from contributors, we require a signed [Contributor License Agreement (CLA)](http://tableau.github.io/contributing.html),\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftableau%2Fts-checked-fsm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftableau%2Fts-checked-fsm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftableau%2Fts-checked-fsm/lists"}