Effect-TS vs fp-ts vs Zod: Functional TypeScript Patterns 2026
Compare Effect-TS, fp-ts, and Zod for functional programming in TypeScript. Learn error handling, dependency injection, schema validation, and when to use each library in 2026.
Functional programming in TypeScript has never been more accessible β or more confusing. You have three major libraries competing for the same mental space: Effect-TS (a full functional runtime), fp-ts (pure functional programming primitives), and Zod (runtime schema validation with TypeScript inference). Developers frequently ask: are these alternatives to each other? Should I use all three? Which one do I actually need?
This guide cuts through the confusion with concrete code examples, honest trade-offs, and clear use-case recommendations for 2026. Use our TypeScript Playground or JSON Schema Validator to test the examples as you read.
What Each Library Actually Does
Before comparing them, itβs critical to understand what problem each library solves:
| Library | Primary purpose | Replaces |
|---|---|---|
| Zod | Runtime schema validation + TypeScript type inference | Joi, Yup, manual validation |
| fp-ts | Functional programming primitives (Option, Either, pipe) | Custom FP utilities, Maybe types |
| Effect-TS | Full functional runtime (errors, async, DI, observability) | Promise chains, try/catch, fp-ts |
They are not pure alternatives. Zod is a validation library. fp-ts provides FP building blocks. Effect-TS is a full application runtime. You might use Zod alone, or Zod + Effect, or just Effect (which has its own schema library).
At a Glance: Feature Comparison
| Feature | Effect-TS | fp-ts | Zod |
|---|---|---|---|
| Error handling | β Typed errors in Effect | β Either type | β throws / .safeParse |
| Async support | β Built-in (Effect) | β οΈ TaskEither | β No |
| Schema validation | β @effect/schema | β No | β Core feature |
| Dependency injection | β Layer/Context | β No | β No |
| Observability/tracing | β Built-in | β No | β No |
| Bundle size | Large (~150KB) | Medium (~60KB) | Small (~12KB) |
| Learning curve | Very steep | Steep | Low |
| Tree-shakeable | β | β | β |
| Type inference quality | Excellent | Very good | Excellent |
Zod: The Pragmatic Choice
Zod is the most widely adopted of the three. Its design philosophy is simple: declare a schema, validate data at runtime, and get full TypeScript types for free β with zero code generation.
Basic Schema Validation
import { z } from "zod";
// Define schema once β TypeScript type is inferred automatically
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(255),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(["admin", "user", "viewer"]),
createdAt: z.coerce.date(),
});
// TypeScript type inferred from schema β no duplication
type User = z.infer<typeof UserSchema>;
// Runtime validation
const result = UserSchema.safeParse(rawData);
if (result.success) {
const user: User = result.data; // Fully typed
console.log(user.email);
} else {
console.error(result.error.format());
// { email: { _errors: ["Invalid email"] }, ... }
}
Parsing API Request Bodies (Express/Hono)
import { z } from "zod";
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
password: z.string().min(8).regex(/[A-Z]/, "Must contain uppercase"),
});
const app = new Hono();
app.post(
"/users",
zValidator("json", CreateUserSchema),
async (c) => {
const { name, email, password } = c.req.valid("json");
// data is fully typed β TypeScript knows all fields here
const user = await createUser({ name, email, password });
return c.json(user, 201);
}
);
Advanced: Transform & Refine
const PasswordSchema = z
.object({
password: z.string().min(8),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords must match",
path: ["confirmPassword"],
});
const DateRangeSchema = z
.object({
start: z.coerce.date(),
end: z.coerce.date(),
})
.transform((data) => ({
...data,
durationMs: data.end.getTime() - data.start.getTime(),
}));
When to use Zod
- API request/response validation in any framework
- Environment variable parsing (
import { z } from "zod"; z.string().url().parse(process.env.DATABASE_URL)) - Form validation (React Hook Form + Zod is a standard combo)
- Configuration schemas
- Anywhere you receive untrusted data from the outside world
fp-ts: Functional Programming Primitives
fp-ts brings Haskell-style type classes to TypeScript β Option (Maybe), Either, Task, IO, and more. It enables total, compositional functions without exceptions.
Option: Safe Nullable Values
import { option as O, pipe } from "fp-ts";
// Instead of: User | null | undefined
// Use: Option<User>
function findUser(id: string): O.Option<User> {
const user = db.get(id);
return user ? O.some(user) : O.none;
}
// Chain operations safely β if any step returns none, the whole chain is none
const emailOrDefault = pipe(
findUser("user-123"),
O.map((user) => user.email),
O.filter((email) => email.includes("@company.com")),
O.getOrElse(() => "[email protected]")
);
Either: Typed Error Handling
import { either as E, pipe } from "fp-ts";
type ValidationError = { field: string; message: string };
type DatabaseError = { code: string; message: string };
function validateEmail(email: string): E.Either<ValidationError, string> {
return email.includes("@")
? E.right(email)
: E.left({ field: "email", message: "Invalid email format" });
}
async function saveUser(
data: UserCreate
): Promise<E.Either<ValidationError | DatabaseError, User>> {
return pipe(
validateEmail(data.email),
E.chain(() => validateName(data.name)),
// E.chain allows you to sequence operations that may fail
E.map((validData) => ({ ...validData, id: uuid() }))
);
}
TaskEither: Async + Either Combined
import { taskEither as TE, pipe } from "fp-ts";
type AppError = { type: "DB_ERROR" | "NOT_FOUND"; message: string };
const fetchUser = (id: string): TE.TaskEither<AppError, User> =>
TE.tryCatch(
() => db.users.findById(id),
(err): AppError => ({ type: "DB_ERROR", message: String(err) })
);
const getProfile = (id: string): TE.TaskEither<AppError, UserProfile> =>
pipe(
fetchUser(id),
TE.chain((user) =>
user
? TE.right(buildProfile(user))
: TE.left({ type: "NOT_FOUND", message: `User ${id} not found` })
)
);
// Execute the task
const result = await getProfile("user-123")();
if (E.isRight(result)) {
console.log(result.right); // UserProfile
} else {
console.error(result.left); // AppError
}
When to use fp-ts
- Teams with functional programming background (Haskell, Scala, Elm)
- Codebases where null/undefined bugs are common
- Domain logic that benefits from compositional, pure functions
- When you want explicit error types without exceptions
The fp-ts Problem in 2026
fp-ts has largely been superseded by Effect-TS for new projects. The fp-ts author (Giulio Canti) now works on Effect, and fp-ts v3 development has stalled. Many fp-ts patterns (TaskEither, ReaderTaskEither) are verbose compared to their Effect equivalents.
Effect-TS: The Full Functional Runtime
Effect-TS is the most ambitious of the three. Itβs not just a library β itβs a complete application runtime that handles:
- Typed errors (like Either)
- Async operations (like Task/Promise)
- Dependency injection (like a DI container)
- Resource management (like bracket/finalizer patterns)
- Observability (built-in tracing and logging)
- Concurrency (fibers, structured concurrency)
Basic Effect
import { Effect, pipe } from "effect";
// Effect<A, E, R> β A = success type, E = error type, R = requirements (DI)
type DatabaseError = { _tag: "DatabaseError"; message: string };
type NotFoundError = { _tag: "NotFoundError"; id: string };
const fetchUser = (id: string): Effect.Effect<User, DatabaseError | NotFoundError> =>
Effect.tryPromise({
try: () => db.users.findById(id),
catch: (err): DatabaseError => ({
_tag: "DatabaseError",
message: String(err),
}),
}).pipe(
Effect.flatMap((user) =>
user
? Effect.succeed(user)
: Effect.fail({ _tag: "NotFoundError", id } as NotFoundError)
)
);
Error Handling with Pattern Matching
import { Effect, Match } from "effect";
const program = pipe(
fetchUser("user-123"),
Effect.catchTag("NotFoundError", (err) =>
Effect.succeed({ id: err.id, name: "Anonymous", email: "" })
),
Effect.catchTag("DatabaseError", (err) =>
Effect.fail(new Error(`Fatal: ${err.message}`))
)
);
Dependency Injection with Layer/Context
import { Effect, Layer, Context } from "effect";
// Define a service interface
class UserRepository extends Context.Tag("UserRepository")<
UserRepository,
{
findById: (id: string) => Effect.Effect<User, NotFoundError>;
save: (user: User) => Effect.Effect<User, DatabaseError>;
}
>() {}
// Create a live implementation
const UserRepositoryLive = Layer.succeed(UserRepository, {
findById: (id) =>
Effect.tryPromise({
try: () => db.users.findById(id),
catch: () => ({ _tag: "NotFoundError", id } as NotFoundError),
}),
save: (user) =>
Effect.tryPromise({
try: () => db.users.save(user),
catch: (err) => ({ _tag: "DatabaseError", message: String(err) } as DatabaseError),
}),
});
// Use the service
const createUser = (data: UserCreate): Effect.Effect<User, DatabaseError, UserRepository> =>
pipe(
UserRepository,
Effect.flatMap((repo) => repo.save({ ...data, id: uuid() }))
);
// Compose and run
const main = pipe(
createUser({ name: "Alice", email: "[email protected]" }),
Effect.provide(UserRepositoryLive)
);
Effect.runPromise(main).then(console.log);
@effect/schema: Built-in Schema Validation
Effect ships its own schema library that integrates natively with its error model:
import { Schema } from "@effect/schema";
import { Effect } from "effect";
const UserSchema = Schema.Struct({
id: Schema.UUID,
name: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(255)),
email: Schema.String.pipe(Schema.pattern(/.+@.+/)),
role: Schema.Literal("admin", "user", "viewer"),
});
type User = Schema.Schema.Type<typeof UserSchema>;
// Decode returns an Effect β errors are typed
const decodeUser = Schema.decodeUnknown(UserSchema);
const program = pipe(
decodeUser(rawBody),
Effect.flatMap((user) => saveUser(user)),
Effect.catchAll((err) => Effect.logError("Validation failed", err))
);
When to use Effect-TS
- Greenfield TypeScript backend with complex business logic
- Teams committed to functional programming patterns
- Applications where observability, error traceability, and DI are critical
- Replacing tangled try/catch + promise chains in existing services
- You want structured concurrency (fibers) without raw async/await pitfalls
Which Library for Which Scenario?
Scenario 1: API Request Validation
Use Zod. Itβs the industry standard, integrates with every framework (tRPC, Hono, Fastify, Next.js), and is the simplest tool for the job.
// Simple, effective, no overhead
const body = CreateUserSchema.parse(req.body);
Scenario 2: Domain Logic with Error Propagation
Use Effect-TS or fp-ts. Both allow you to compose operations that may fail without throwing exceptions.
// Effect-TS approach β preferred for new projects
const processOrder = (id: string) =>
pipe(
fetchOrder(id),
Effect.flatMap(validateOrder),
Effect.flatMap(chargePayment),
Effect.flatMap(sendConfirmationEmail)
);
Scenario 3: Environment / Config Validation
Use Zod. Parse environment variables at startup and fail fast with clear messages.
const Env = z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(["development", "production", "test"]),
});
export const env = Env.parse(process.env);
Scenario 4: Legacy Codebase Migration
Start with Zod for boundary validation. Incrementally add Effect for core services. Avoid fp-ts for new code β its maintenance status is uncertain.
Scenario 5: tRPC or Full-Stack TypeScript
Use Zod β tRPC is built on Zod and expects it for input/output schemas.
The 2026 Recommendation
For most TypeScript projects:
-
Always use Zod for runtime validation at system boundaries (API inputs, env vars, config files, external API responses).
-
Use Effect-TS if youβre building complex backend services where you want typed errors, DI, and structured async. It has a steep learning curve but pays off in large codebases.
-
Avoid starting new projects with fp-ts β the community has largely moved to Effect. If you have existing fp-ts code, it still works, but donβt expand it.
-
You can combine Zod + Effect β use Zod for the βedgeβ (HTTP boundaries) and Effect for the βcoreβ (business logic, services).
// The modern combo: Zod at the edge, Effect in the core
app.post("/orders", async (req, res) => {
const body = CreateOrderSchema.parse(req.body); // Zod validates input
const result = await Effect.runPromise( // Effect handles business logic
pipe(
validateInventory(body.items),
Effect.flatMap(calculateTotal),
Effect.flatMap(createOrder)
)
);
res.json(result);
});
Getting Started
# Zod (always recommended)
npm install zod
# Effect-TS (for functional runtime)
npm install effect @effect/schema
# fp-ts (for FP primitives, if not using Effect)
npm install fp-ts
Learning Resources
- Zod docs β start here for schema validation
- Effect docs β comprehensive guides and cookbook
- fp-ts docs β still relevant for understanding the patterns
Validate your schemas and types as you learn with our TypeScript Playground and JSON Formatter.
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.