Skip to content

OOFP — Functional TypeScript

Type-safe functional patterns that make code easier to write, review, and trust — whether written by you or your AI.
$npm install @oofp/core
npm versionMIT LicenseTypeScriptTree-shakeable

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.

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
}
}
// 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.
Error Handling

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 | ParseError
Dependency Injection

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 boundary
const user = await pipe(
findUser("123"),
RTE.run({ db, logger }),
TE.toPromise,
);
Composition

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 step
const emails = pipe(
users,
L.filter((u) => u.active),
L.map((u) => u.email),
L.take(5),
);
// flow: create reusable pipelines
const getActiveEmails = flow(
L.filter((u: User) => u.active),
L.map((u) => u.email),
);
// Maybe: safe access without null
const name = pipe(
M.fromNullable(user.nickname),
M.map((n) => n.toUpperCase()),
M.getOrElse("Anonymous"),
);

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.