OOFP — Functional TypeScript
Why Functional?
Section titled “Why Functional?”Functional patterns replace fragile imperative code with predictable, composable pipelines. The types tell you everything — what can fail, what dependencies are needed, and what the output will be.
Error Handling
Section titled “Error Handling”async function getUser(id: string) { try { const response = await fetch(`/api/users/${id}`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); try { return validateUser(data); } catch (e) { throw new Error(`Validation failed: ${e}`); } } catch (e) { // What type is e? Who knows. // Did we handle all cases? Maybe. console.error("Something went wrong", e); return null; // Now callers need null checks everywhere }}const getUser = (id: string) => pipe( TE.tryCatch(() => fetch(`/api/users/${id}`)), TE.chain((res) => res.ok ? TE.tryCatch(() => res.json()) : TE.left(HttpError.fromStatus(res.status)) ), TE.chain(validateUser), // Error type is explicit: HttpError | ValidationError // No null. No thrown exceptions. No guessing. );Dependency Injection
Section titled “Dependency Injection”// Pass dependencies through every function call...async function createOrder( db: Database, logger: Logger, mailer: Mailer, cache: Cache, userId: string, items: Item[]) { logger.info("Creating order"); const user = await db.findUser(userId); const order = await db.createOrder(user, items); await mailer.send(user.email, orderConfirmation(order)); await cache.invalidate(`user:${userId}:orders`); return order;}// 6 parameters. Every caller needs all dependencies.// Testing means mocking everything manually.interface OrderCtx { db: Database; logger: Logger; mailer: Mailer; cache: Cache;}
const createOrder = (userId: string, items: Item[]) => pipe( RTE.ask<OrderCtx>(), RTE.tapRTE((ctx) => ctx.logger.info("Creating order")), RTE.chaint((ctx) => ctx.db.findUser(userId)), RTE.chaint((user) => createAndNotify(user, items)), );// Dependencies injected once at the boundary.// Each function only declares what it needs.Type-safe errors with Either & TaskEither
Section titled “Type-safe errors with Either & TaskEither”Errors are values, not exceptions. Either<E, A> encodes success and failure in the type system, so the compiler ensures you handle every case. TaskEither extends this to async operations.
No try/catch guessing. No unknown error types. The signature tells you exactly what can go wrong.
import * as TE from "@oofp/core/task-either";import * as E from "@oofp/core/either";import { pipe } from "@oofp/core/pipe";
const data = await pipe( TE.tryCatch(() => fetchData(url)), TE.chain(parseResponse), TE.map(transform), TE.toPromise,);// Rejects on Left, resolves on Right.// Error type is always explicit: HttpError | ParseErrorBuilt-in DI with ReaderTaskEither
Section titled “Built-in DI with ReaderTaskEither”ReaderTaskEither<R, E, A> combines dependency injection, async execution, and typed errors in a single composable type. Define your dependencies as interfaces, inject them once at the application boundary.
No DI containers. No decorators. No runtime magic. Just types and functions.
import * as RTE from "@oofp/core/reader-task-either";import * as TE from "@oofp/core/task-either";
interface AppCtx { db: Database; logger: Logger }
const findUser = (id: string) => pipe( RTE.ask<AppCtx>(), RTE.chaint((ctx) => ctx.db.findUser(id)), RTE.map(toUserDTO), );
// Inject dependencies at the boundaryconst user = await pipe( findUser("123"), RTE.run({ db, logger }), TE.toPromise,);Composable by design with pipe & flow
Section titled “Composable by design with pipe & flow”pipe pushes a value through a chain of functions. flow creates a new function from a chain without needing an initial value. Everything in OOFP is built to compose — monads, utilities, transformers.
Small functions, clear data flow, easy to read top-to-bottom.
import { pipe } from "@oofp/core/pipe";import { flow } from "@oofp/core/flow";import * as L from "@oofp/core/list";import * as M from "@oofp/core/maybe";
// pipe: transform a value step by stepconst emails = pipe( users, L.filter((u) => u.active), L.map((u) => u.email), L.take(5),);
// flow: create reusable pipelinesconst getActiveEmails = flow( L.filter((u: User) => u.active), L.map((u) => u.email),);
// Maybe: safe access without nullconst name = pipe( M.fromNullable(user.nickname), M.map((n) => n.toUpperCase()), M.getOrElse("Anonymous"),);Built for the AI era
Section titled “Built for the AI era”Functional code is not just better for humans — it is structurally easier for AI tools to generate, and for you to verify.
Easier to review
Functional pipelines read top to bottom. Each step does one thing. When an AI generates a pipe chain, you can verify each transformation independently — no hidden control flow to trace.
Less room for bugs
Error handling and null safety are encoded in types, not in discipline. The compiler catches what code review misses — whether the code was written by you or by a language model.
You architect, AI implements
Define your types, interfaces, and pipeline structure. Let AI tools fill in the transformations. The type system ensures the pieces fit together correctly.