Name the two values the create form will need: committed todos and the text being typed. Add these contracts in src/application.ts.
Name the application state
import { Context, Effect } from "effect";
import { RefSubject } from "@typed/fx";
import * as Domain from "./domain.js";
// Layers supply these cells, so each application or test can own fresh state.
export class TodoList extends RefSubject.Service<TodoList, Domain.TodoList>()("TodoList") {}
export class TodoText extends RefSubject.Service<TodoText, string>()("TodoText") {}
TodoList and TodoText name capabilities; declaring them does not allocate a global list or input. A Layer will supply their values when the application runs. The same action can then use empty state in the browser or prepared state in a test.
Keep the values separate: submitting text changes the list, but typing does not. Counts and filtered rows will be derived from that list later.
Ask for a new Todo
export class CreateTodo extends Context.Service<
CreateTodo,
(text: string) => Effect.Effect<Domain.Todo>
>()("CreateTodo") {}
Creation needs an ID and a timestamp. CreateTodo lets the application ask for a complete Todo without choosing how those values are produced. Infrastructure can use browser crypto and a clock; a test can return predictable values.
The return type says creation is an Effect. Merely requesting the function does not run it. The next chapter will call it after checking the draft.
Check your understanding: if two independent applications provide different TodoList layers, do they share one global array? No: each provided state instance owns its own value. See shared state contracts for the Layer mechanics.
Complete files
Keep the files from the previous step and replace or add these. Each full file is the source used by this milestone; the excerpts above select lines from it.
src/domain.ts
import * as Schema from "effect/Schema";
// Titles can change or match; the ID remains the item's identity.
export const TodoId = Schema.String.pipe(Schema.brand("TodoId"));
export type TodoId = typeof TodoId.Type;
export const Todo = Schema.Struct({
id: TodoId,
text: Schema.String,
completed: Schema.Boolean,
timestamp: Schema.DateTimeUtcFromString,
});
export type Todo = typeof Todo.Type;
export const TodoList = Schema.Array(Todo);
export type TodoList = typeof TodoList.Type;
export const FilterState = Schema.Literals(["all", "active", "completed"]);
export type FilterState = typeof FilterState.Type;
// Keep untouched item objects, even though the array itself changes.
export const updateTodo =
(id: TodoId, f: (todo: Todo) => Todo) =>
(list: TodoList): TodoList =>
list.map((todo) => (todo.id === id ? f(todo) : todo));
export const editText = (id: TodoId, text: string) => updateTodo(id, (todo) => ({ ...todo, text }));
export const updateText =
(text: string) =>
(todo: Todo): Todo => ({ ...todo, text });
export const toggleCompleted =
(id: TodoId) =>
(list: TodoList): TodoList =>
updateTodo(id, (todo) => ({ ...todo, completed: !todo.completed }))(list);
export const deleteTodo =
(id: TodoId) =>
(list: TodoList): TodoList =>
list.filter((todo) => todo.id !== id);
export const clearCompleted = (list: TodoList): TodoList => list.filter((todo) => !todo.completed);
export const activeCount = (list: TodoList): number =>
list.filter((todo) => !todo.completed).length;
export const someAreCompleted = (list: TodoList): boolean => list.some((todo) => todo.completed);
// An empty list should not check the "mark all" control.
export const allAreCompleted = (list: TodoList): boolean =>
list.length > 0 && list.every((todo) => todo.completed);
export const toggleAllCompleted = (list: TodoList): TodoList => {
const completed = list.some((todo) => !todo.completed);
return list.map((todo) => ({ ...todo, completed }));
};
export const filterTodoList = ({ list, state }: { list: TodoList; state: FilterState }) =>
state === "active"
? list.filter((todo) => !todo.completed)
: state === "completed"
? list.filter((todo) => todo.completed)
: list;
src/application.ts
import { Context, Effect } from "effect";
import { RefSubject } from "@typed/fx";
import * as Domain from "./domain.js";
// Layers supply these cells, so each application or test can own fresh state.
export class TodoList extends RefSubject.Service<TodoList, Domain.TodoList>()("TodoList") {}
export class TodoText extends RefSubject.Service<TodoText, string>()("TodoText") {}
export class CreateTodo extends Context.Service<
CreateTodo,
(text: string) => Effect.Effect<Domain.Todo>
>()("CreateTodo") {}