{"id":39547842,"url":"https://github.com/savkelita/tea-effect","last_synced_at":"2026-01-18T06:49:22.394Z","repository":{"id":333028287,"uuid":"1129092497","full_name":"savkelita/tea-effect","owner":"savkelita","description":"The Elm Architecture for TypeScript with Effect","archived":false,"fork":false,"pushed_at":"2026-01-17T09:42:50.000Z","size":147,"stargazers_count":2,"open_issues_count":2,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-17T11:22:05.130Z","etag":null,"topics":["effect-ts","elm","functional-programming","react","tea-effect","the-elm-architecture","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/savkelita.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-06T15:40:46.000Z","updated_at":"2026-01-17T09:42:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/savkelita/tea-effect","commit_stats":null,"previous_names":["savkelita/tea-effect"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/savkelita/tea-effect","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/savkelita%2Ftea-effect","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/savkelita%2Ftea-effect/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/savkelita%2Ftea-effect/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/savkelita%2Ftea-effect/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/savkelita","download_url":"https://codeload.github.com/savkelita/tea-effect/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/savkelita%2Ftea-effect/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28532028,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-18T00:39:45.795Z","status":"online","status_checked_at":"2026-01-18T02:00:07.578Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["effect-ts","elm","functional-programming","react","tea-effect","the-elm-architecture","typescript"],"created_at":"2026-01-18T06:49:21.798Z","updated_at":"2026-01-18T06:49:22.381Z","avatar_url":"https://github.com/savkelita.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tea-effect\n\nThe Elm Architecture for TypeScript with [Effect](https://effect.website/).\n\nA spiritual successor to [elm-ts](https://github.com/gcanti/elm-ts), replacing fp-ts/RxJS with the Effect ecosystem.\n\n## Why tea-effect?\n\n- **Type-safe side effects** - Commands and subscriptions with full type inference\n- **Elm-style HTTP** - Declarative requests with Schema validation\n- **Dependency injection** - Effect's built-in `R` (requirements) for testable code\n- **Structured concurrency** - Effect's runtime handles cancellation and resource cleanup\n- **React integration** - Ready-to-use hooks for React applications\n\n## Installation\n\n```sh\nnpm install tea-effect effect @effect/platform\n# or\nyarn add tea-effect effect @effect/platform\n```\n\nNote: `effect` and `@effect/platform` are peer dependencies\n\n## Differences from elm-ts\n\n- `Effect` instead of `fp-ts` + `RxJS`\n- `@effect/schema` instead of `io-ts` for runtime validation\n- Http module with Elm-style API\n\n## React\n\n```tsx\nimport * as TeaReact from \"tea-effect/React\";\nimport { Effect } from \"effect\";\nimport { createRoot } from \"react-dom/client\";\nimport * as Counter from \"./Counter\";\n\nconst root = createRoot(document.getElementById(\"app\")!);\n\nEffect.runPromise(\n  TeaReact.run(\n    TeaReact.program(Counter.init, Counter.update, Counter.view),\n    (dom) =\u003e root.render(dom),\n  ),\n);\n```\n\n## Counter Example\n\n```tsx\n// Counter.tsx\nimport * as Cmd from \"tea-effect/Cmd\";\nimport * as TeaReact from \"tea-effect/React\";\n\nexport type Model = { count: number };\n\nexport type Msg = { type: \"Increment\" } | { type: \"Decrement\" };\n\nexport const init: [Model, Cmd.Cmd\u003cMsg\u003e] = [{ count: 0 }, Cmd.none];\n\nexport const update = (msg: Msg, model: Model): [Model, Cmd.Cmd\u003cMsg\u003e] =\u003e {\n  switch (msg.type) {\n    case \"Increment\":\n      return [{ count: model.count + 1 }, Cmd.none];\n    case \"Decrement\":\n      return [{ count: model.count - 1 }, Cmd.none];\n  }\n};\n\nexport const view =\n  (model: Model): TeaReact.Html\u003cMsg\u003e =\u003e\n  (dispatch) =\u003e (\n    \u003cdiv\u003e\n      \u003cbutton onClick={() =\u003e dispatch({ type: \"Decrement\" })}\u003e-\u003c/button\u003e\n      \u003cspan\u003e{model.count}\u003c/span\u003e\n      \u003cbutton onClick={() =\u003e dispatch({ type: \"Increment\" })}\u003e+\u003c/button\u003e\n    \u003c/div\u003e\n  );\n```\n\n## Http Example\n\ntea-effect provides an Elm-inspired Http module for type-safe HTTP requests with Schema validation.\n\n```tsx\n// Users.tsx\nimport { Schema, Option, pipe } from \"effect\";\nimport * as Cmd from \"tea-effect/Cmd\";\nimport * as Http from \"tea-effect/Http\";\nimport * as TeaReact from \"tea-effect/React\";\n\nconst User = Schema.Struct({\n  id: Schema.Number,\n  name: Schema.String,\n});\n\ntype User = Schema.Schema.Type\u003ctypeof User\u003e;\n\nexport type Model = {\n  users: User[];\n  loading: boolean;\n  error: Option.Option\u003cHttp.HttpError\u003e;\n};\n\nexport type Msg =\n  | { type: \"FetchUsers\" }\n  | { type: \"GotUsers\"; users: User[] }\n  | { type: \"GotError\"; error: Http.HttpError };\n\nconst fetchUsers = pipe(\n  Http.get(\"/api/users\", Http.expectJson(Schema.Array(User))),\n  Http.withTimeout(5000),\n);\n\nconst renderError = (error: Http.HttpError): string =\u003e {\n  switch (error._tag) {\n    case \"BadUrl\":\n      return `Invalid URL: ${error.url}`;\n    case \"Timeout\":\n      return \"Request timed out\";\n    case \"NetworkError\":\n      return \"Network error - check your connection\";\n    case \"BadStatus\":\n      return `Server error: ${error.status}`;\n    case \"BadBody\":\n      return `Invalid response: ${error.error}`;\n  }\n};\n\nconst renderErrorMessage = (error: Option.Option\u003cHttp.HttpError\u003e) =\u003e\n  pipe(\n    error,\n    Option.match({\n      onNone: () =\u003e null,\n      onSome: (e) =\u003e \u003cp\u003e{renderError(e)}\u003c/p\u003e,\n    }),\n  );\n\nexport const init: [Model, Cmd.Cmd\u003cMsg\u003e] = [\n  { users: [], loading: false, error: Option.none() },\n  Cmd.none,\n];\n\nexport const update = (msg: Msg, model: Model): [Model, Cmd.Cmd\u003cMsg\u003e] =\u003e {\n  switch (msg.type) {\n    case \"FetchUsers\":\n      return [\n        { ...model, loading: true, error: Option.none() },\n        Http.send(fetchUsers, {\n          onSuccess: (users): Msg =\u003e ({ type: \"GotUsers\", users }),\n          onError: (error): Msg =\u003e ({ type: \"GotError\", error }),\n        }),\n      ];\n    case \"GotUsers\":\n      return [{ ...model, loading: false, users: msg.users }, Cmd.none];\n    case \"GotError\":\n      return [\n        { ...model, loading: false, error: Option.some(msg.error) },\n        Cmd.none,\n      ];\n  }\n};\n\nexport const view =\n  (model: Model): TeaReact.Html\u003cMsg\u003e =\u003e\n  (dispatch) =\u003e (\n    \u003cdiv\u003e\n      \u003cbutton\n        onClick={() =\u003e dispatch({ type: \"FetchUsers\" })}\n        disabled={model.loading}\n      \u003e\n        {model.loading ? \"Loading...\" : \"Fetch Users\"}\n      \u003c/button\u003e\n      {renderErrorMessage(model.error)}\n      \u003cul\u003e\n        {model.users.map((user) =\u003e (\n          \u003cli key={user.id}\u003e{user.name}\u003c/li\u003e\n        ))}\n      \u003c/ul\u003e\n    \u003c/div\u003e\n  );\n```\n\n## Subscriptions Example\n\nSubscriptions let you listen to external events like timers, keyboard, or WebSocket messages.\n\n```tsx\n// Timer.tsx\nimport * as Cmd from \"tea-effect/Cmd\";\nimport * as Sub from \"tea-effect/Sub\";\nimport * as TeaReact from \"tea-effect/React\";\n\nexport type Model = {\n  seconds: number;\n  running: boolean;\n};\n\nexport type Msg = { type: \"Tick\" } | { type: \"Toggle\" } | { type: \"Reset\" };\n\nexport const init: [Model, Cmd.Cmd\u003cMsg\u003e] = [\n  { seconds: 0, running: false },\n  Cmd.none,\n];\n\nexport const update = (msg: Msg, model: Model): [Model, Cmd.Cmd\u003cMsg\u003e] =\u003e {\n  switch (msg.type) {\n    case \"Tick\":\n      return [{ ...model, seconds: model.seconds + 1 }, Cmd.none];\n    case \"Toggle\":\n      return [{ ...model, running: !model.running }, Cmd.none];\n    case \"Reset\":\n      return [{ ...model, seconds: 0 }, Cmd.none];\n  }\n};\n\nexport const subscriptions = (model: Model): Sub.Sub\u003cMsg\u003e =\u003e\n  model.running ? Sub.interval(1000, { type: \"Tick\" }) : Sub.none;\n\nexport const view =\n  (model: Model): TeaReact.Html\u003cMsg\u003e =\u003e\n  (dispatch) =\u003e (\n    \u003cdiv\u003e\n      \u003cp\u003e{model.seconds}s\u003c/p\u003e\n      \u003cbutton onClick={() =\u003e dispatch({ type: \"Toggle\" })}\u003e\n        {model.running ? \"Stop\" : \"Start\"}\n      \u003c/button\u003e\n      \u003cbutton onClick={() =\u003e dispatch({ type: \"Reset\" })}\u003eReset\u003c/button\u003e\n    \u003c/div\u003e\n  );\n```\n\n## elm-ts vs tea-effect\n\n| Feature              | elm-ts          | tea-effect        |\n| -------------------- | --------------- | ----------------- |\n| FP library           | fp-ts           | Effect            |\n| Streaming            | RxJS Observable | Effect Stream     |\n| Error handling       | `Either\u003cE, A\u003e`  | `Effect\u003cA, E, R\u003e` |\n| Dependency injection | Reader pattern  | Built-in `R` type |\n| Runtime validation   | io-ts           | @effect/schema    |\n| Resource management  | Manual          | Scope (automatic) |\n\n## Module Structure\n\n| Module         | Description                                   |\n| -------------- | --------------------------------------------- |\n| `Cmd`          | Commands - side effects that produce messages |\n| `Sub`          | Subscriptions - streams of external events    |\n| `Task`         | Tasks - convert Effects to Commands           |\n| `Http`         | HTTP requests with Schema validation          |\n| `LocalStorage` | Browser storage with Schema encoding          |\n| `Platform`     | Core TEA program runtime                      |\n| `Html`         | Programs with view rendering                  |\n| `React`        | React integration and hooks                   |\n\n## Requirements\n\n- Node.js 18+\n- TypeScript 5.3+\n- tsconfig.json:\n\n```json\n{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"exactOptionalPropertyTypes\": true\n  }\n}\n```\n\n## Examples\n\n- [tea-effect-realworld](https://github.com/savkelita/tea-effect-realworld) - Real-world examples with Counter, Http, and Subscriptions\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsavkelita%2Ftea-effect","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsavkelita%2Ftea-effect","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsavkelita%2Ftea-effect/lists"}