Europe/Zurich
BlogSeptember 23, 2025

What TypeScript is for JavaScript, Effect is for TypeScript

Levin Bänninger
What TypeScript is for JavaScript, Effect is for TypeScript
It was a random Tuesday when I saw the tweet that would completely change how I think about TypeScript. Matt Pocock, the person who literally built his career teaching advanced TypeScript patterns, posted something that stopped me mid-scroll:
What TS is for JS, Effect is for TS
Matt PocockTwitter
The cognitive dissonance was immediate. Here's someone who taught me conditional types, template literal types, and every advanced TypeScript pattern I know, suddenly suggesting there's something beyond TypeScript? If the person who made me feel like a TypeScript wizard was questioning the foundations, maybe I should pay attention. That tweet hit me like a revelation. Just as TypeScript gave JavaScript the safety net it desperately needed, Effect does the same for TypeScript. But what does that actually mean? TypeScript took JavaScript's chaos and added types. It made runtime errors into compile-time errors. It gave us IntelliSense that actually worked. But TypeScript still left massive holes in our safety net—holes that Effect was built to fill.

// This looks safe, right?
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return data as User;
}

// Usage feels type-safe
const user = await fetchUser("123");
console.log(user.name); // What could go wrong?
The TypeScript version compiles perfectly. It also fails spectacularly in production. The Effect version forces you to confront reality: network requests fail, APIs return garbage, and pretending otherwise is just wishful thinking. One of my biggest frustrations with TypeScript has been the ecosystem fragmentation. You pick Zod for validation, Axios for HTTP, Jest for testing, and pray they work well together. Effect takes a radically different approach.
TypeScript's Error Handling
Typescript
try {
  const user = await fetchUser(id);
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts[0].id);
  return comments;
} catch (error) {
  // What kind of error? From which operation?
  console.error("Something went wrong:", error);
  return [];
}
Problems:
  • No type information about possible errors
  • Can't distinguish between different failure types
  • Error context is lost
  • Recovery strategies are ad-hoc
Effect's Error Handling
Typescript
const pipeline = (id: string) =>
  fetchUser(id).pipe(
    Effect.flatMap(user => fetchPosts(user.id)),
    Effect.flatMap(posts =>
      posts.length > 0
        ? fetchComments(posts[0].id)
        : Effect.fail(new NoPostsError())
    ),
    Effect.catchTag("ValidationError", () =>
      Effect.succeed([])
    ),
    Effect.retry({ times: 3 }),
    Effect.timeout("5 seconds")
  );
Benefits:
  • Errors are part of the type signature
  • Specific error handling per error type
  • Composable retry and timeout strategies
  • Railway-oriented programming
Effect has a steep learning curve, but it's not complexity for complexity's sake. It's the complexity that was always there, just hidden.
My realization after two weeks
Learning Effect was humbling. Here I was, thinking I understood TypeScript pretty well, and suddenly I'm confronted with concepts like:
  • Higher-kinded types (even though TypeScript doesn't really support them)
  • Effect composition that actually maintains type safety
  • Dependency injection that doesn't rely on magic decorators
  • Resource management that prevents memory leaks
But here's the thing: every one of these concepts solved real problems I had in my TypeScript projects. Problems I didn't even realize I had. The breakthrough came when I rewrote a simple API client. Here's what happened:

class UserService {
  async createUser(userData: CreateUserRequest): Promise<User> {
    const response = await this.http.post('/users', userData);
    return response.data;
  }
}
The TypeScript version looked cleaner, but it was lying. The Effect version forced me to handle:
  • Input validation
  • Network timeouts
  • HTTP errors
  • Response parsing
  • Output validation
The difference becomes stark when you're building systems that:
  • Process financial transactions
  • Handle user authentication
  • Integrate with external APIs
  • Need to recover from partial failures
  • Require observability and monitoring
TypeScript gives you the illusion of safety. Effect gives you actual safety. Matt's analogy becomes crystal clear here: JavaScript developers thought they were safe until TypeScript showed them they weren't. TypeScript developers think they're safe until Effect shows them they aren't. What fascinated me most about Effect wasn't just the error handling—it was how everything fits together:
Typescript
const program = Effect.gen(function* (_) {
  // Configuration with validation
  const config = yield* _(Config.all({
    port: Config.number("PORT").pipe(Config.withDefault(3000)),
    dbUrl: Config.string("DATABASE_URL"),
    apiKey: Config.secret("API_KEY")
  }));

  // HTTP server with proper error handling
  const server = yield* _(
    HttpServer.serve(
      HttpRouter.empty.pipe(
        HttpRouter.get("/users/:id",
          (req) => fetchUser(req.params.id)
        )
      )
    ).pipe(
      HttpServer.withLogAddress
    )
  );

  yield* _(Effect.log("Server running on port " + config.port));
  yield* _(Effect.never);
}).pipe(
  Effect.provide(HttpServer.layer),
  Effect.provide(NodeContext.layer)
);

// Run with proper error handling and resource cleanup
Effect.runMain(program);
This is a complete, production-ready server setup. Notice:
  • Configuration is validated at startup
  • Errors are handled at every level
  • Resources are properly managed
  • Logging is built-in
  • The type system tracks everything
Matt Pocock's insight about Effect wasn't just about discovering a new library—it was recognizing the natural evolution of type safety. His tweet "What TS is for JS, Effect is for TS" captures something profound:
Just like TypeScript didn't replace JavaScript overnight, Effect won't replace TypeScript immediately. But the pattern is clear: each step makes visible the problems we didn't know we had. If you're curious about Effect, here's my advice:
  1. Start small: Pick one async operation and rewrite it in Effect
  2. Embrace the learning curve: The concepts are worth understanding
  3. Focus on error handling first: That's where you'll see immediate benefits
  4. Use the ecosystem: Effect Schema, Effect Platform, etc. work better together
The TypeScript community has spent years building complex solutions to work around the language's limitations. Effect suggests a different approach: what if we just used a better foundation?
On this page