https://github.com/savkelita/tea-effect
The Elm Architecture for TypeScript with Effect
https://github.com/savkelita/tea-effect
effect-ts elm functional-programming react tea-effect the-elm-architecture typescript
Last synced: 2 months ago
JSON representation
The Elm Architecture for TypeScript with Effect
- Host: GitHub
- URL: https://github.com/savkelita/tea-effect
- Owner: savkelita
- Created: 2026-01-06T15:40:46.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-01-17T09:42:50.000Z (2 months ago)
- Last Synced: 2026-01-17T11:22:05.130Z (2 months ago)
- Topics: effect-ts, elm, functional-programming, react, tea-effect, the-elm-architecture, typescript
- Language: TypeScript
- Homepage:
- Size: 144 KB
- Stars: 2
- Watchers: 0
- Forks: 1
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# tea-effect
The Elm Architecture for TypeScript with [Effect](https://effect.website/).
A spiritual successor to [elm-ts](https://github.com/gcanti/elm-ts), replacing fp-ts/RxJS with the Effect ecosystem.
## Why tea-effect?
- **Type-safe side effects** - Commands and subscriptions with full type inference
- **Elm-style HTTP** - Declarative requests with Schema validation
- **Dependency injection** - Effect's built-in `R` (requirements) for testable code
- **Structured concurrency** - Effect's runtime handles cancellation and resource cleanup
- **React integration** - Ready-to-use hooks for React applications
## Installation
```sh
npm install tea-effect effect @effect/platform
# or
yarn add tea-effect effect @effect/platform
```
Note: `effect` and `@effect/platform` are peer dependencies
## Differences from elm-ts
- `Effect` instead of `fp-ts` + `RxJS`
- `@effect/schema` instead of `io-ts` for runtime validation
- Http module with Elm-style API
## React
```tsx
import * as TeaReact from "tea-effect/React";
import { Effect } from "effect";
import { createRoot } from "react-dom/client";
import * as Counter from "./Counter";
const root = createRoot(document.getElementById("app")!);
Effect.runPromise(
TeaReact.run(
TeaReact.program(Counter.init, Counter.update, Counter.view),
(dom) => root.render(dom),
),
);
```
## Counter Example
```tsx
// Counter.tsx
import * as Cmd from "tea-effect/Cmd";
import * as TeaReact from "tea-effect/React";
export type Model = { count: number };
export type Msg = { type: "Increment" } | { type: "Decrement" };
export const init: [Model, Cmd.Cmd] = [{ count: 0 }, Cmd.none];
export const update = (msg: Msg, model: Model): [Model, Cmd.Cmd] => {
switch (msg.type) {
case "Increment":
return [{ count: model.count + 1 }, Cmd.none];
case "Decrement":
return [{ count: model.count - 1 }, Cmd.none];
}
};
export const view =
(model: Model): TeaReact.Html =>
(dispatch) => (
dispatch({ type: "Decrement" })}>-
{model.count}
dispatch({ type: "Increment" })}>+
);
```
## Http Example
tea-effect provides an Elm-inspired Http module for type-safe HTTP requests with Schema validation.
```tsx
// Users.tsx
import { Schema, Option, pipe } from "effect";
import * as Cmd from "tea-effect/Cmd";
import * as Http from "tea-effect/Http";
import * as TeaReact from "tea-effect/React";
const User = Schema.Struct({
id: Schema.Number,
name: Schema.String,
});
type User = Schema.Schema.Type;
export type Model = {
users: User[];
loading: boolean;
error: Option.Option;
};
export type Msg =
| { type: "FetchUsers" }
| { type: "GotUsers"; users: User[] }
| { type: "GotError"; error: Http.HttpError };
const fetchUsers = pipe(
Http.get("/api/users", Http.expectJson(Schema.Array(User))),
Http.withTimeout(5000),
);
const renderError = (error: Http.HttpError): string => {
switch (error._tag) {
case "BadUrl":
return `Invalid URL: ${error.url}`;
case "Timeout":
return "Request timed out";
case "NetworkError":
return "Network error - check your connection";
case "BadStatus":
return `Server error: ${error.status}`;
case "BadBody":
return `Invalid response: ${error.error}`;
}
};
const renderErrorMessage = (error: Option.Option) =>
pipe(
error,
Option.match({
onNone: () => null,
onSome: (e) =>
{renderError(e)}
,
}),
);
export const init: [Model, Cmd.Cmd] = [
{ users: [], loading: false, error: Option.none() },
Cmd.none,
];
export const update = (msg: Msg, model: Model): [Model, Cmd.Cmd] => {
switch (msg.type) {
case "FetchUsers":
return [
{ ...model, loading: true, error: Option.none() },
Http.send(fetchUsers, {
onSuccess: (users): Msg => ({ type: "GotUsers", users }),
onError: (error): Msg => ({ type: "GotError", error }),
}),
];
case "GotUsers":
return [{ ...model, loading: false, users: msg.users }, Cmd.none];
case "GotError":
return [
{ ...model, loading: false, error: Option.some(msg.error) },
Cmd.none,
];
}
};
export const view =
(model: Model): TeaReact.Html =>
(dispatch) => (
dispatch({ type: "FetchUsers" })}
disabled={model.loading}
>
{model.loading ? "Loading..." : "Fetch Users"}
{renderErrorMessage(model.error)}
{model.users.map((user) => (
- {user.name}
))}
);
```
## Subscriptions Example
Subscriptions let you listen to external events like timers, keyboard, or WebSocket messages.
```tsx
// Timer.tsx
import * as Cmd from "tea-effect/Cmd";
import * as Sub from "tea-effect/Sub";
import * as TeaReact from "tea-effect/React";
export type Model = {
seconds: number;
running: boolean;
};
export type Msg = { type: "Tick" } | { type: "Toggle" } | { type: "Reset" };
export const init: [Model, Cmd.Cmd] = [
{ seconds: 0, running: false },
Cmd.none,
];
export const update = (msg: Msg, model: Model): [Model, Cmd.Cmd] => {
switch (msg.type) {
case "Tick":
return [{ ...model, seconds: model.seconds + 1 }, Cmd.none];
case "Toggle":
return [{ ...model, running: !model.running }, Cmd.none];
case "Reset":
return [{ ...model, seconds: 0 }, Cmd.none];
}
};
export const subscriptions = (model: Model): Sub.Sub =>
model.running ? Sub.interval(1000, { type: "Tick" }) : Sub.none;
export const view =
(model: Model): TeaReact.Html =>
(dispatch) => (
{model.seconds}s
dispatch({ type: "Toggle" })}>
{model.running ? "Stop" : "Start"}
dispatch({ type: "Reset" })}>Reset
);
```
## elm-ts vs tea-effect
| Feature | elm-ts | tea-effect |
| -------------------- | --------------- | ----------------- |
| FP library | fp-ts | Effect |
| Streaming | RxJS Observable | Effect Stream |
| Error handling | `Either` | `Effect` |
| Dependency injection | Reader pattern | Built-in `R` type |
| Runtime validation | io-ts | @effect/schema |
| Resource management | Manual | Scope (automatic) |
## Module Structure
| Module | Description |
| -------------- | --------------------------------------------- |
| `Cmd` | Commands - side effects that produce messages |
| `Sub` | Subscriptions - streams of external events |
| `Task` | Tasks - convert Effects to Commands |
| `Http` | HTTP requests with Schema validation |
| `LocalStorage` | Browser storage with Schema encoding |
| `Platform` | Core TEA program runtime |
| `Html` | Programs with view rendering |
| `React` | React integration and hooks |
## Requirements
- Node.js 18+
- TypeScript 5.3+
- tsconfig.json:
```json
{
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": true
}
}
```
## Examples
- [tea-effect-realworld](https://github.com/savkelita/tea-effect-realworld) - Real-world examples with Counter, Http, and Subscriptions
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
MIT