Skip to main content

TS Ep 62: The Effect Type Signature

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 62: This Article
If you understand Effect<A, E, R>, you understand Effect-TS. This single generic type signature is the engine that drives absolute, mathematical predictability in your applications.

TL;DR (Quick Summary)
#

  • The Core Type: Every operation in Effect-TS returns an Effect<SuccessType, ErrorType, Requirements>.
  • A (Success): What the program returns if it succeeds (analogous to the T in Promise<T>).
  • E (Error): The specific, typed errors the program might fail with (analogous to Checked Exceptions).
  • R (Requirement): The context or services the program needs to run (e.g., a Database Connection or a Logger). This is native Dependency Injection.
  • The Magic: The TypeScript compiler mathematically calculates the exact union of all Errors (E) and all Requirements (R) across your entire pipeline automatically.

1. Introduction: The Limitation of Promises
#

To appreciate the genius of the Effect type signature, we must first look at what it replaces: the native Promise.

When you hover over an async function in your IDE, you might see this:

function fetchUser(id: string): Promise<User>

This tells us exactly one thing: if this function succeeds, it gives us a User.

But what if the network fails? What if the user doesn’t exist in the database? What if the database connection isn’t configured? The Promise signature is completely silent. It hides the complexity of your architecture behind a false sense of security.


2. Dissecting Effect<A, E, R>
#

In Effect-TS, every pure function you write will return an Effect. The Effect interface takes three generic type parameters:

import { Effect } from "effect";

// Effect<A, E, R>

Let’s break down each parameter using a real-world example: an HTTP request that fetches a User.

Parameter 1: A (The Success Type)
#

The A parameter represents the value that the Effect will yield if it succeeds. This is exactly like the T in Promise<T>.

If our program fetches a User successfully, the A type is User.

// A program that always succeeds with a User, never fails, and requires nothing.
const successProgram: Effect.Effect<User, never, never> = Effect.succeed({ name: "Rachmat" });

(Note: When a parameter is not used, Effect uses the never type. A failure of type never means the program cannot fail!)

Parameter 2: E (The Error Type)
#

The E parameter represents the specific, typed errors that can cause the Effect to fail. This is where Effect massively upgrades standard TypeScript.

If our program fails to parse the JSON, the E type might be SyntaxError.

// A program that always fails with a string, never succeeds, and requires nothing.
const failureProgram: Effect.Effect<never, string, never> = Effect.fail("Network Timeout");

Parameter 3: R (The Requirement / Context Type)
#

The R parameter represents the contextual environment or dependencies the Effect needs to execute. This is built-in Dependency Injection.

If our program needs a DatabaseService to run, the R type is DatabaseService.

// We will learn how to build this in the Context episode!
// A program that succeeds with a User, fails with a DbError, and requires a DatabaseService.
declare const fetchFromDb: Effect.Effect<User, DbError, DatabaseService>;

3. The Magic of Type Inference and Unions
#

The true power of Effect<A, E, R> is not writing it out manually; the true power is letting the TypeScript compiler infer it for you.

When you use pipe to combine multiple Effects together, TypeScript automatically calculates the union of all possible Successes, all possible Errors, and all required Dependencies!

Step-by-Step: Pipeline Inference
#

Let’s imagine we have three tiny services.

declare const parseId: (input: string) => Effect.Effect<number, "InvalidID", never>;
declare const fetchUser: (id: number) => Effect.Effect<User, "UserNotFound", DatabaseService>;
declare const logUser: (user: User) => Effect.Effect<void, never, LoggerService>;

Notice the specific Errors ("InvalidID", "UserNotFound") and the specific Requirements (DatabaseService, LoggerService).

Let’s pipe them together!

import { Effect } from "effect";

// 🟢 The compiler automatically calculates the final signature!
const finalProgram = Effect.pipe(
  parseId("123"),
  Effect.flatMap(id => fetchUser(id)),
  Effect.flatMap(user => logUser(user))
);

If you hover over finalProgram in VSCode, you will see something magnificent:

// Inferred Type:
Effect.Effect<
  void,                                  // The final Success (from logUser)
  "InvalidID" | "UserNotFound",          // The UNION of all possible errors!
  DatabaseService | LoggerService        // The UNION of all required services!
>

Why this is revolutionary
#

  1. You cannot forget an error: If you want to return an HTTP 200 response to a user, the compiler will refuse to compile until you explicitly write an Effect.catchAll block that handles both "InvalidID" and "UserNotFound".
  2. You cannot forget a dependency: If you try to run Effect.runPromise(finalProgram), the compiler will block you! It will demand that you provide implementations for the DatabaseService and LoggerService first.

4. Aliases for Common Signatures
#

Writing Effect.Effect<A, E, R> constantly can be verbose. Effect provides built-in type aliases for common situations.

AliasFull SignatureWhat it means
Effect.Effect<A>Effect<A, never, never>Succeeds with A, never fails, requires nothing.
Effect.Effect<A, E>Effect<A, E, never>Succeeds with A, fails with E, requires nothing.

(Note: In modern versions of Effect, the Effect type itself has defaults, so Effect.Effect<string> is perfectly valid.)


5. Troubleshooting & Common Errors
#

Error 1: Failing to Provide Requirements
#

TS2345: Argument of type 'Effect<void, never, DatabaseService>' is not assignable to parameter of type 'Effect<void, never, never>'.

The Cause: You attempted to execute an Effect using Effect.runPromise() or Effect.runSync(), but the R parameter is not never. The program requires a DatabaseService to run! The Fix: You must “provide” the service to the pipeline before running it. We will cover how to inject services deeply in Episode 64: Context & Dependency Injection.

// 🔴 Bad: Trying to run a program that lacks its dependencies
Effect.runPromise(finalProgram); 

// 🟢 Good: Providing the dependency first (Preview for upcoming episodes!)
const executableProgram = Effect.provideService(finalProgram, DatabaseService, myDbImpl);
Effect.runPromise(executableProgram);

Error 2: Type Inference collapses to any or unknown
#

The Cause: You piped a function that lacks a strict return type, causing the E or R inference calculation to collapse. The Fix: Ensure every function returning an Effect is strictly typed, especially when passing inline callbacks to flatMap.


Summary & Next Steps
#

In this episode:

  • We discovered how native Promises hide architectural complexity.
  • We dissected the Effect<A, E, R> signature into Success (A), Errors (E), and Requirements (R).
  • We saw how TypeScript automatically calculates the mathematical union of all errors and requirements across a complex pipeline.
  • We learned about Effect type aliases to reduce verbosity.

Now that we understand the type signature, how do we actually create Effects? How do we wrap legacy Promises, synchronous code, and callback APIs into the Effect ecosystem?

In Episode 63: Creating and Running Effects, we will master the art of wrapping the outside world into pure functional boxes!

typescript - This article is part of a series.
Part 62: This Article