Effective Modules

A more ergonomic way to write Effective code


(Note: this article assumes familiarity with TypeScript, Effect, and SOLID).
To jump straight to the tool I built, click here.

Since late 2025, I’ve been moving all my personal projects over to Effect, along with several work projects at Flexport (before getting laid off a couple months ago). I’ve come to largely agree with the premise that Effect is

the missing TypeScript standard library

In 2026, serious developers who understand the value of a strongly-typed language consider all these table stakes for a robust, type-safe programming platform. All these are missing from vanilla TypeScript. A comprehensive standard library should include all these features, and Effect has done it for the TypeScript ecosystem, arguably outdoing the ecosystem alternatives on every front. Effect has brought DI into the type system. That is, there is a compile-time failure when required dependencies aren’t provided. In general, few DI systems across all major programming languages offer this highest level of correctness.

So, Effect seems like the right choice for writing TypeScript at scale. However, as I adopt Effect conventions and constructs such as Service, Context, and Layer across my projects, I keep running into the same awkward developer experiences.

Headaches

1. Decoupling Impl from Interface

In Effect v3, we are encouraged to use the Effect.Service utility to improve code succinctness. Provide a default implementation of a Service and its interface gets inferred.

import { import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
} from "effect";
import { import FileSystemFileSystem } from "@effect/platform"; import { import NodeFileSystemNodeFileSystem } from "@effect/platform-node" class class CacheCache extends import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const Service: <Cache>() => {
    <Key, Make>(key: Key, make: Make): Effect.Service.Class<Cache, Key, Make>;
    <Key, Make>(key: Key, make: Make): Effect.Service.Class<Cache, Key, Make>;
    <Key, Make>(key: Key, make: Make): Effect.Service.Class<Cache, Key, Make>;
    <Key, Make>(key: Key, make: Make): Effect.Service.Class<Cache, Key, Make>;
    <Key, Make>(key: Key, make: Make): Effect.Service.Class<...>;
}

Simplifies the creation and management of services in Effect by defining both a Tag and a Layer.

Details

This function allows you to streamline the creation of services by combining the definition of a Context.Tag and a Layer in a single step. It supports various ways of providing the service implementation:

  • Using an effect to define the service dynamically.
  • Using sync or succeed to define the service statically.
  • Using scoped to create services with lifecycle management.

It also allows you to specify dependencies for the service, which will be provided automatically when the service is used. Accessors can be optionally generated for the service, making it more convenient to use.

Example

import { Effect } from 'effect';

class Prefix extends Effect.Service<Prefix>()("Prefix", {
 sync: () => ({ prefix: "PRE" })
}) {}

class Logger extends Effect.Service<Logger>()("Logger", {
 accessors: true,
 effect: Effect.gen(function* () {
   const { prefix } = yield* Prefix
   return {
     info: (message: string) =>
       Effect.sync(() => {
         console.log(`[${prefix}][${message}]`)
       })
   }
 }),
 dependencies: [Prefix.Default]
}) {}
@since3.9.0@categoryContext@experimentalmight be up for breaking changes
Service
<class CacheCache>()("app/Cache", {
// Define how to create the service
effect: Effect.Effect<{
    readonly lookup: (key: string) => Effect.Effect<string, PlatformError, never>;
}, never, FileSystem.FileSystem>
effect
: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const gen: <YieldWrap<Tag<FileSystem.FileSystem, FileSystem.FileSystem>>, {
    readonly lookup: (key: string) => Effect.Effect<string, PlatformError, never>;
}>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Tag<FileSystem.FileSystem, FileSystem.FileSystem>>, {
    readonly lookup: (key: string) => Effect.Effect<string, PlatformError, never>;
}, never>) => Effect.Effect<{
    readonly lookup: (key: string) => Effect.Effect<string, PlatformError, never>;
}, never, FileSystem.FileSystem> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"

const addServiceCharge = (amount: number) => amount + 1

const applyDiscount = (
  total: number,
  discountRate: number
): Effect.Effect<number, Error> =>
  discountRate === 0
    ? Effect.fail(new Error("Discount rate cannot be zero"))
    : Effect.succeed(total - (total * discountRate) / 100)

const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))

const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))

export const program = Effect.gen(function* () {
  const transactionAmount = yield* fetchTransactionAmount
  const discountRate = yield* fetchDiscountRate
  const discountedAmount = yield* applyDiscount(
    transactionAmount,
    discountRate
  )
  const finalAmount = addServiceCharge(discountedAmount)
  return `Final amount to charge: ${finalAmount}`
})
@since2.0.0@categoryCreating Effects
gen
(function* () {
const const fs: FileSystem.FileSystemfs = yield* import FileSystemFileSystem.const FileSystem: Tag<FileSystem.FileSystem, FileSystem.FileSystem>
@since1.0.0@categorymodel@since1.0.0@categorytag
FileSystem
;
const const lookup: (key: string) => Effect.Effect<string, PlatformError, never>lookup = (key: stringkey: string) => const fs: FileSystem.FileSystemfs.FileSystem.readFileString: (path: string, encoding?: string) => Effect.Effect<string, PlatformError>

Read the contents of a file.

readFileString
(`cache/${key: stringkey}`);
return { lookup: (key: string) => Effect.Effect<string, PlatformError, never>lookup } as
type const = {
    readonly lookup: (key: string) => Effect.Effect<string, PlatformError, never>;
}
const
;
}), // Specify dependencies dependencies: readonly [Layer<FileSystem.FileSystem, never, never>]dependencies: [import NodeFileSystemNodeFileSystem.const layer: Layer<FileSystem.FileSystem, never, never>
@since1.0.0@categorylayer
layer
]
}) {} import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const gen: <YieldWrap<Tag<Cache, Cache>> | YieldWrap<Effect.Effect<string, PlatformError, never>>, void>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Tag<Cache, Cache>> | YieldWrap<Effect.Effect<string, PlatformError, never>>, void, never>) => Effect.Effect<void, PlatformError, Cache> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"

const addServiceCharge = (amount: number) => amount + 1

const applyDiscount = (
  total: number,
  discountRate: number
): Effect.Effect<number, Error> =>
  discountRate === 0
    ? Effect.fail(new Error("Discount rate cannot be zero"))
    : Effect.succeed(total - (total * discountRate) / 100)

const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))

const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))

export const program = Effect.gen(function* () {
  const transactionAmount = yield* fetchTransactionAmount
  const discountRate = yield* fetchDiscountRate
  const discountedAmount = yield* applyDiscount(
    transactionAmount,
    discountRate
  )
  const finalAmount = addServiceCharge(discountedAmount)
  return `Final amount to charge: ${finalAmount}`
})
@since2.0.0@categoryCreating Effects
gen
(function*() {
const const cache: Cachecache = yield* class CacheCache; yield* const cache: Cachecache.
lookup: (key: string) => Effect.Effect<string, PlatformError, never>
lookup
("some key");
});

While this does improve readability (compared to declaring the Tag and Layer separately), the trade-off is violation of a sacred SOLID pillar, the Dependency Inversion principle, which mandates that maintainable code ought to depend on abstractions, not concretions. If a bug is introduced into the implementation, a compiler error should ideally appear in the implementation block and the interface should not change. But with Effect.Service an error may instead appear in client code because changing the implementation might inadvertently also change the interface.

class class CacheCache extends import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const Service: <Cache>() => {
    <Key, Make>(key: Key, make: Make): Effect.Service.Class<Cache, Key, Make>;
    <Key, Make>(key: Key, make: Make): Effect.Service.Class<Cache, Key, Make>;
    <Key, Make>(key: Key, make: Make): Effect.Service.Class<Cache, Key, Make>;
    <Key, Make>(key: Key, make: Make): Effect.Service.Class<Cache, Key, Make>;
    <Key, Make>(key: Key, make: Make): Effect.Service.Class<...>;
}

Simplifies the creation and management of services in Effect by defining both a Tag and a Layer.

Details

This function allows you to streamline the creation of services by combining the definition of a Context.Tag and a Layer in a single step. It supports various ways of providing the service implementation:

  • Using an effect to define the service dynamically.
  • Using sync or succeed to define the service statically.
  • Using scoped to create services with lifecycle management.

It also allows you to specify dependencies for the service, which will be provided automatically when the service is used. Accessors can be optionally generated for the service, making it more convenient to use.

Example

import { Effect } from 'effect';

class Prefix extends Effect.Service<Prefix>()("Prefix", {
 sync: () => ({ prefix: "PRE" })
}) {}

class Logger extends Effect.Service<Logger>()("Logger", {
 accessors: true,
 effect: Effect.gen(function* () {
   const { prefix } = yield* Prefix
   return {
     info: (message: string) =>
       Effect.sync(() => {
         console.log(`[${prefix}][${message}]`)
       })
   }
 }),
 dependencies: [Prefix.Default]
}) {}
@since3.9.0@categoryContext@experimentalmight be up for breaking changes
Service
<class CacheCache>()("app/Cache", {
effect: Effect.Effect<{
    readonly lockup: (key: string) => Effect.Effect<string, PlatformError, never>;
}, never, FileSystem.FileSystem>
effect
: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const gen: <YieldWrap<Tag<FileSystem.FileSystem, FileSystem.FileSystem>>, {
    readonly lockup: (key: string) => Effect.Effect<string, PlatformError, never>;
}>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Tag<FileSystem.FileSystem, FileSystem.FileSystem>>, {
    readonly lockup: (key: string) => Effect.Effect<string, PlatformError, never>;
}, never>) => Effect.Effect<{
    readonly lockup: (key: string) => Effect.Effect<string, PlatformError, never>;
}, never, FileSystem.FileSystem> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"

const addServiceCharge = (amount: number) => amount + 1

const applyDiscount = (
  total: number,
  discountRate: number
): Effect.Effect<number, Error> =>
  discountRate === 0
    ? Effect.fail(new Error("Discount rate cannot be zero"))
    : Effect.succeed(total - (total * discountRate) / 100)

const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))

const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))

export const program = Effect.gen(function* () {
  const transactionAmount = yield* fetchTransactionAmount
  const discountRate = yield* fetchDiscountRate
  const discountedAmount = yield* applyDiscount(
    transactionAmount,
    discountRate
  )
  const finalAmount = addServiceCharge(discountedAmount)
  return `Final amount to charge: ${finalAmount}`
})
@since2.0.0@categoryCreating Effects
gen
(function* () {
const const fs: FileSystem.FileSystemfs = yield* import FileSystemFileSystem.const FileSystem: Tag<FileSystem.FileSystem, FileSystem.FileSystem>
@since1.0.0@categorymodel@since1.0.0@categorytag
FileSystem
;
// Accidentally changed the spelling of "lookup" to "lockup" const const lockup: (key: string) => Effect.Effect<string, PlatformError, never>lockup = (key: stringkey: string) => const fs: FileSystem.FileSystemfs.FileSystem.readFileString: (path: string, encoding?: string) => Effect.Effect<string, PlatformError>

Read the contents of a file.

readFileString
(`cache/${key: stringkey}`);
return { lockup: (key: string) => Effect.Effect<string, PlatformError, never>lockup } as
type const = {
    readonly lockup: (key: string) => Effect.Effect<string, PlatformError, never>;
}
const
;
}), dependencies: readonly [Layer<FileSystem.FileSystem, never, never>]dependencies: [import NodeFileSystemNodeFileSystem.const layer: Layer<FileSystem.FileSystem, never, never>
@since1.0.0@categorylayer
layer
]
}) {} import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const gen: <any, void>(f: (resume: Effect.Adapter) => Generator<any, void, never>) => Effect.Effect<void, unknown, unknown> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"

const addServiceCharge = (amount: number) => amount + 1

const applyDiscount = (
  total: number,
  discountRate: number
): Effect.Effect<number, Error> =>
  discountRate === 0
    ? Effect.fail(new Error("Discount rate cannot be zero"))
    : Effect.succeed(total - (total * discountRate) / 100)

const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))

const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))

export const program = Effect.gen(function* () {
  const transactionAmount = yield* fetchTransactionAmount
  const discountRate = yield* fetchDiscountRate
  const discountedAmount = yield* applyDiscount(
    transactionAmount,
    discountRate
  )
  const finalAmount = addServiceCharge(discountedAmount)
  return `Final amount to charge: ${finalAmount}`
})
@since2.0.0@categoryCreating Effects
gen
(function*() {
const const cache: Cachecache = yield* class CacheCache; yield* const cache: Cachecache.lookup("some key");
Property 'lookup' does not exist on type 'Cache'. Did you mean 'lockup'?
});

I deem Effect v3’s Effect.Service an anti-pattern, but if it’s so problematic why was it introduced at all?

2. Awkward Interface Syntax

Effect.Service was introduced because the right way to do things sucks. In Effect, declaring an interface (Tag) looks like this

type 
type ICache = {
    lookup(key: string): Effect.Effect<string, PlatformError, never>;
}
ICache
= {
function lookup(key: string): Effect.Effect<string, PlatformError, never>lookup(key: stringkey: string): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0@categoryModels@since2.0.0
Effect
<string, type PlatformError = BadArgument | SystemError
@since1.0.0@categoryModels@since1.0.0@categoryModels
PlatformError
, never>
} class class CacheCache extends Context.Tag<Cache, ICache>("app/Cache") {}
Expected 1 type arguments, but got 2.
Type '<Self, Shape>() => TagClass<Self, Cache, Shape>' is not a constructor function type.

Oh wait no it’s

class class CacheCache extends import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const Tag: <Cache>(id: Cache) => <Self, Shape>() => Context.TagClass<Self, Cache, Shape>
@example
import * as assert from "node:assert"
import { Context, Layer } from "effect"

class MyTag extends Context.Tag("MyTag")<
 MyTag,
 { readonly myNum: number }
>() {
 static Live = Layer.succeed(this, { myNum: 108 })
}
@since2.0.0@categoryconstructors
Tag
<Cache, ICache>()("app/Cache") {}
Expected 0 arguments, but got 1.
Expected 1 type arguments, but got 2.

Still not it. Maybe

class class CacheCache extends import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const Tag: <"app/Cache">(id: "app/Cache") => <Self, Shape>() => Context.TagClass<Self, "app/Cache", Shape>
@example
import * as assert from "node:assert"
import { Context, Layer } from "effect"

class MyTag extends Context.Tag("MyTag")<
 MyTag,
 { readonly myNum: number }
>() {
 static Live = Layer.succeed(this, { myNum: 108 })
}
@since2.0.0@categoryconstructors
Tag
("app/Cache")<class CacheCache,
type ICache = {
    lookup(key: string): Effect.Effect<string, PlatformError, never>;
}
ICache
>() {}

there we go.

… this syntax is hieroglyphic. When defining a new Tag I typically fight with the compiler for a minute or so because the syntax just doesn’t commit to memory. It’s repetitive and cluttered.

  • Why am I creating a class?
  • Why am I passing the class in as a parameter?
  • Why am I repeating the name of my interface 5 times?
  • What are all these function calls for?

There are, as one might expect, reasonably good answers to these questions.

  • Effect needs a way to uniquely identify a Tag at compile time such that two Tags with overlapping shapes can’t be mistakenly substituted for one another due to structural typing. Effect solves this with the key field being preserved in the type structure. In this case app/Cache is the unique string.
  • Classes are both compile time and runtime entities so by declaring Cache as the Tag then passing it in as a generic for the Self type, it can be used as both a compile time reference to the Tag in Effect requirements channels (e.g. Effect<void, never, Cache>) and in runtime code when acquiring the dependency (e.g. yield* Cache).

For such a high cost in syntax complexity, Tag still leaves much to be desired. For instance, using string literals is not a foolproof way to guarantee Tag uniqueness. Ideally Effect would use nominal types for that. By adding a private field to every class declared using the Tag() method, we could accomplish this:

Before

class class CacheCache extends import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const Tag: <"app/Cache">(id: "app/Cache") => <Self, Shape>() => Context.TagClass<Self, "app/Cache", Shape>
@example
import * as assert from "node:assert"
import { Context, Layer } from "effect"

class MyTag extends Context.Tag("MyTag")<
 MyTag,
 { readonly myNum: number }
>() {
 static Live = Layer.succeed(this, { myNum: 108 })
}
@since2.0.0@categoryconstructors
Tag
("app/Cache")
<class CacheCache, ICache>() {} // Imagine we accidentally create another tag with the same name class class Cache2Cache2 extends import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const Tag: <"app/Cache">(id: "app/Cache") => <Self, Shape>() => Context.TagClass<Self, "app/Cache", Shape>
@example
import * as assert from "node:assert"
import { Context, Layer } from "effect"

class MyTag extends Context.Tag("MyTag")<
 MyTag,
 { readonly myNum: number }
>() {
 static Live = Layer.succeed(this, { myNum: 108 })
}
@since2.0.0@categoryconstructors
Tag
("app/Cache")
<class Cache2Cache2, ICache>() {} const const program: Effect.Effect<void, never, Cache>program: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0@categoryModels@since2.0.0
Effect
<void, never, class CacheCache> = import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const gen: <YieldWrap<Context.Tag<Cache2, ICache>>, void>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Context.Tag<Cache2, ICache>>, void, never>) => Effect.Effect<void, never, Cache2> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"

const addServiceCharge = (amount: number) => amount + 1

const applyDiscount = (
  total: number,
  discountRate: number
): Effect.Effect<number, Error> =>
  discountRate === 0
    ? Effect.fail(new Error("Discount rate cannot be zero"))
    : Effect.succeed(total - (total * discountRate) / 100)

const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))

const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))

export const program = Effect.gen(function* () {
  const transactionAmount = yield* fetchTransactionAmount
  const discountRate = yield* fetchDiscountRate
  const discountedAmount = yield* applyDiscount(
    transactionAmount,
    discountRate
  )
  const finalAmount = addServiceCharge(discountedAmount)
  return `Final amount to charge: ${finalAmount}`
})
@since2.0.0@categoryCreating Effects
gen
(function*() {
// No compiler error?! This shouldn't be allowed! yield* class Cache2Cache2; });

After

class class CacheCache extends import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const Tag: <"app/Cache">(id: "app/Cache") => <Self, Shape>() => Context.TagClass<Self, "app/Cache", Shape>
@example
import * as assert from "node:assert"
import { Context, Layer } from "effect"

class MyTag extends Context.Tag("MyTag")<
 MyTag,
 { readonly myNum: number }
>() {
 static Live = Layer.succeed(this, { myNum: 108 })
}
@since2.0.0@categoryconstructors
Tag
("app/Cache")
<class CacheCache, ICache>() {private Cache.nominal: booleannominal = true;} class class Cache2Cache2 extends import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const Tag: <"app/Cache">(id: "app/Cache") => <Self, Shape>() => Context.TagClass<Self, "app/Cache", Shape>
@example
import * as assert from "node:assert"
import { Context, Layer } from "effect"

class MyTag extends Context.Tag("MyTag")<
 MyTag,
 { readonly myNum: number }
>() {
 static Live = Layer.succeed(this, { myNum: 108 })
}
@since2.0.0@categoryconstructors
Tag
("app/Cache")
<class Cache2Cache2, ICache>() {private Cache2.nominal: booleannominal = true;} const program: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0@categoryModels@since2.0.0
Effect
<void, never, class CacheCache> = import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const gen: <YieldWrap<Context.Tag<Cache2, ICache>>, void>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Context.Tag<Cache2, ICache>>, void, never>) => Effect.Effect<void, never, Cache2> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"

const addServiceCharge = (amount: number) => amount + 1

const applyDiscount = (
  total: number,
  discountRate: number
): Effect.Effect<number, Error> =>
  discountRate === 0
    ? Effect.fail(new Error("Discount rate cannot be zero"))
    : Effect.succeed(total - (total * discountRate) / 100)

const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))

const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))

export const program = Effect.gen(function* () {
  const transactionAmount = yield* fetchTransactionAmount
  const discountRate = yield* fetchDiscountRate
  const discountedAmount = yield* applyDiscount(
    transactionAmount,
    discountRate
  )
  const finalAmount = addServiceCharge(discountedAmount)
  return `Final amount to charge: ${finalAmount}`
})
@since2.0.0@categoryCreating Effects
gen
(function*() {
Type 'Effect<void, never, Cache2>' is not assignable to type 'Effect<void, never, Cache>'. Type 'Cache2' is not assignable to type 'Cache'. Types have separate declarations of a private property 'nominal'.
// Now we get an error. That's better yield* class Cache2Cache2; });

but this would make the syntax even nastier.

All this to say, Effect’s v3 Tag and v4 Service utilities had me missing the simplicity of pure interfaces and wondering if it might be possible to get the properties Effect is aiming for without the unreadable and repetitive syntax.

3. Error Noise

This biggest headache I experience, by far, when working with Effect is my IDE becoming unusable whenever I introduce a bug into an effect implementation that changes its error or requirements channel. This happens a lot during refactors, especially when work on one service leads me to refactor the contract (interface) of another.

In this example from one of my own projects, this is what happens when I change the type signature of the github service’s resolveAccessToken method so that it has an additional requirement such that it no longer matches ensureLoggedIn’s requirements channel:

Seeing the whole Layer implementation light up as incorrect is not conducive to productively finding and fixing the bug. Ideally only the resolveAccessToken line would be highlighted as wrong, prompting me to provide a Context with the required Service to that Effect.

When one runs into this kind of issue frequently, it becomes tempting to start taking the path of least resistance. That often looks like handling all possible errors after the entire Effect’s body rather than handling each error at the line which produces it; over time these kinds of shortcuts lead to brittle and confusing code.

4. Verbose Dependency Passing

Building on that resolveAccessToken example, when you want to yield an Effect which depends on 1+ services you need to either define that Effect within the Layer’s closure so the services yielded at layer construction time are in scope, or you need to create a custom Context and provide it to the Effect before yielding.

Here’s an example of that first option

class class ServiceOneServiceOne extends import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const Tag: <"ServiceOne">(id: "ServiceOne") => <Self, Shape>() => Context.TagClass<Self, "ServiceOne", Shape>
@example
import * as assert from "node:assert"
import { Context, Layer } from "effect"

class MyTag extends Context.Tag("MyTag")<
 MyTag,
 { readonly myNum: number }
>() {
 static Live = Layer.succeed(this, { myNum: 108 })
}
@since2.0.0@categoryconstructors
Tag
("ServiceOne")<class ServiceOneServiceOne, {
function serviceOneMethod(someInput: number): Effect.Effect<string, never, never>serviceOneMethod(someInput: numbersomeInput: number): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0@categoryModels@since2.0.0
Effect
<string, never, never>;
}>() {}; class class ServiceTwoServiceTwo extends import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const Tag: <"ServiceTwo">(id: "ServiceTwo") => <Self, Shape>() => Context.TagClass<Self, "ServiceTwo", Shape>
@example
import * as assert from "node:assert"
import { Context, Layer } from "effect"

class MyTag extends Context.Tag("MyTag")<
 MyTag,
 { readonly myNum: number }
>() {
 static Live = Layer.succeed(this, { myNum: 108 })
}
@since2.0.0@categoryconstructors
Tag
("ServiceTwo")<class ServiceTwoServiceTwo, {
function serviceTwoMethod(someInput: number): Effect.Effect<string, never, never>serviceTwoMethod(someInput: numbersomeInput: number): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0@categoryModels@since2.0.0
Effect
<string, never, never>;
}>() {}; const const ServiceTwoImpl: Layer.Layer<ServiceTwo, never, ServiceOne>ServiceTwoImpl = import LayerLayer.
const effect: <ServiceTwo, {
    serviceTwoMethod(someInput: number): Effect.Effect<string, never, never>;
}, never, ServiceOne>(tag: Context.Tag<ServiceTwo, {
    serviceTwoMethod(someInput: number): Effect.Effect<string, never, never>;
}>, effect: Effect.Effect<{
    serviceTwoMethod(someInput: number): Effect.Effect<string, never, never>;
}, never, ServiceOne>) => Layer.Layer<ServiceTwo, never, ServiceOne> (+1 overload)

Constructs a layer from the specified effect.

@since2.0.0@categoryconstructors
effect
(class ServiceTwoServiceTwo, import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const gen: <YieldWrap<Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>>, {
    serviceTwoMethod: (someInput: number) => Effect.Effect<string, never, never>;
}>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>>, {
    serviceTwoMethod: (someInput: number) => Effect.Effect<string, never, never>;
}, never>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"

const addServiceCharge = (amount: number) => amount + 1

const applyDiscount = (
  total: number,
  discountRate: number
): Effect.Effect<number, Error> =>
  discountRate === 0
    ? Effect.fail(new Error("Discount rate cannot be zero"))
    : Effect.succeed(total - (total * discountRate) / 100)

const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))

const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))

export const program = Effect.gen(function* () {
  const transactionAmount = yield* fetchTransactionAmount
  const discountRate = yield* fetchDiscountRate
  const discountedAmount = yield* applyDiscount(
    transactionAmount,
    discountRate
  )
  const finalAmount = addServiceCharge(discountedAmount)
  return `Final amount to charge: ${finalAmount}`
})
@since2.0.0@categoryCreating Effects
gen
(function*() {
const
const serviceOne: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}
serviceOne
= yield* class ServiceOneServiceOne;
const
const helperFn: (someInput: number) => Effect.Effect<string, never, never>
helperFn
= import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const fn: <YieldWrap<Effect.Effect<string, never, never>>, string, [someInput: number]>(body: (someInput: number) => Generator<YieldWrap<Effect.Effect<string, never, never>>, string, never>) => (someInput: number) => Effect.Effect<string, never, never> (+20 overloads)fn(function*(someInput: numbersomeInput: number) {
// By placing helperFn inside the layer, it can access serviceOne directly const const serviceOneResult: stringserviceOneResult = yield*
const serviceOne: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}
serviceOne
.function serviceOneMethod(someInput: number): Effect.Effect<string, never, never>serviceOneMethod(someInput: numbersomeInput);
// Do something complex with result before returning it. const const complexResult: stringcomplexResult = const serviceOneResult: stringserviceOneResult; return const complexResult: stringcomplexResult; }); return { serviceTwoMethod: (someInput: number) => Effect.Effect<string, never, never>serviceTwoMethod: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const fn: <YieldWrap<Effect.Effect<string, never, never>>, string, [someInput: number]>(body: (someInput: number) => Generator<YieldWrap<Effect.Effect<string, never, never>>, string, never>) => (someInput: number) => Effect.Effect<string, never, never> (+20 overloads)fn(function*(someInput: numbersomeInput) {
return yield* const helperFn: (someInput: number) => Effect.Effect<string, never, never>helperFn(someInput: numbersomeInput); }) } }));

And here’s option two

const 
const helperFn: (someInput: number) => Effect.Effect<string, never, ServiceOne>
helperFn
= import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const fn: <YieldWrap<Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>> | YieldWrap<Effect.Effect<string, never, never>>, string, [someInput: number]>(body: (someInput: number) => Generator<YieldWrap<Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>> | YieldWrap<Effect.Effect<string, never, never>>, string, never>) => (someInput: number) => Effect.Effect<...> (+20 overloads)
fn
(function*(someInput: numbersomeInput: number) {
const
const serviceOne: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}
serviceOne
= yield* class ServiceOneServiceOne;
const const serviceOneResult: stringserviceOneResult = yield*
const serviceOne: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}
serviceOne
.function serviceOneMethod(someInput: number): Effect.Effect<string, never, never>serviceOneMethod(someInput: numbersomeInput);
// Do something complex with result before returning it. const const complexResult: stringcomplexResult = const serviceOneResult: stringserviceOneResult; return const complexResult: stringcomplexResult; }); const const ServiceTwoImpl: Layer.Layer<ServiceTwo, never, ServiceOne>ServiceTwoImpl = import LayerLayer.
const effect: <ServiceTwo, {
    serviceTwoMethod(someInput: number): Effect.Effect<string, never, never>;
}, never, ServiceOne>(tag: Context.Tag<ServiceTwo, {
    serviceTwoMethod(someInput: number): Effect.Effect<string, never, never>;
}>, effect: Effect.Effect<{
    serviceTwoMethod(someInput: number): Effect.Effect<string, never, never>;
}, never, ServiceOne>) => Layer.Layer<ServiceTwo, never, ServiceOne> (+1 overload)

Constructs a layer from the specified effect.

@since2.0.0@categoryconstructors
effect
(class ServiceTwoServiceTwo, import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const gen: <YieldWrap<Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>>, {
    serviceTwoMethod: (someInput: number) => Effect.Effect<string, never, never>;
}>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>>, {
    serviceTwoMethod: (someInput: number) => Effect.Effect<string, never, never>;
}, never>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"

const addServiceCharge = (amount: number) => amount + 1

const applyDiscount = (
  total: number,
  discountRate: number
): Effect.Effect<number, Error> =>
  discountRate === 0
    ? Effect.fail(new Error("Discount rate cannot be zero"))
    : Effect.succeed(total - (total * discountRate) / 100)

const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))

const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))

export const program = Effect.gen(function* () {
  const transactionAmount = yield* fetchTransactionAmount
  const discountRate = yield* fetchDiscountRate
  const discountedAmount = yield* applyDiscount(
    transactionAmount,
    discountRate
  )
  const finalAmount = addServiceCharge(discountedAmount)
  return `Final amount to charge: ${finalAmount}`
})
@since2.0.0@categoryCreating Effects
gen
(function*() {
const
const serviceOne: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}
serviceOne
= yield* class ServiceOneServiceOne;
// We have to manually create this Context then provide it to the helper function const const context: Context.Context<ServiceOne>context = pipe<Context.Context<never>, Context.Context<ServiceOne>>(a: Context.Context<never>, ab: (a: Context.Context<never>) => Context.Context<ServiceOne>): Context.Context<ServiceOne> (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

Details

The pipe function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together.

import { pipe } from "effect"

const result = pipe(input, func1, func2, ..., funcN)

In this syntax, input is the initial value, and func1, func2, ..., funcN are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned.

Here's an illustration of how pipe works:

┌───────┐    ┌───────┐    ┌───────┐    ┌───────┐    ┌───────┐    ┌────────┐
│ input │───►│ func1 │───►│ func2 │───►│  ...  │───►│ funcN │───►│ result │
└───────┘    └───────┘    └───────┘    └───────┘    └───────┘    └────────┘

It's important to note that functions passed to pipe must have a single argument because they are only called with a single argument.

When to Use

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g)

becomes:

import { pipe, Array } from "effect"

pipe(as, Array.map(f), Array.filter(g))

Example (Chaining Arithmetic Operations)

import { pipe } from "effect"

// Define simple arithmetic operations
const increment = (x: number) => x + 1
const double = (x: number) => x * 2
const subtractTen = (x: number) => x - 10

// Sequentially apply these operations using `pipe`
const result = pipe(5, increment, double, subtractTen)

console.log(result)
// Output: 2
@since2.0.0
pipe
(
import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const empty: () => Context.Context<never>

Returns an empty Context.

@example
import * as assert from "node:assert"
import { Context } from "effect"

assert.strictEqual(Context.isContext(Context.empty()), true)
@since2.0.0@categoryconstructors
empty
(),
import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.
const add: <ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>(tag: Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>, service: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}) => <Services>(self: Context.Context<Services>) => Context.Context<ServiceOne | Services> (+1 overload)

Adds a service to a given Context.

@example
import * as assert from "node:assert"
import { Context, pipe } from "effect"

const Port = Context.GenericTag<{ PORT: number }>("Port")
const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout")

const someContext = Context.make(Port, { PORT: 8080 })

const Services = pipe(
  someContext,
  Context.add(Timeout, { TIMEOUT: 5000 })
)

assert.deepStrictEqual(Context.get(Services, Port), { PORT: 8080 })
assert.deepStrictEqual(Context.get(Services, Timeout), { TIMEOUT: 5000 })
@since2.0.0
add
(class ServiceOneServiceOne,
const serviceOne: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}
serviceOne
)
); return { serviceTwoMethod: (someInput: number) => Effect.Effect<string, never, never>serviceTwoMethod: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const fn: <YieldWrap<Effect.Effect<string, never, never>>, string, [someInput: number]>(body: (someInput: number) => Generator<YieldWrap<Effect.Effect<string, never, never>>, string, never>) => (someInput: number) => Effect.Effect<string, never, never> (+20 overloads)fn(function*(someInput: numbersomeInput) {
// Because helperFn lives outside the impl, we have to pass a context or // eject from DI entirely and pass all services as function params return yield* const helperFn: (someInput: number) => Effect.Effect<string, never, ServiceOne>helperFn(someInput: numbersomeInput).Pipeable.pipe<Effect.Effect<string, never, ServiceOne>, Effect.Effect<string, never, never>>(this: Effect.Effect<string, never, ServiceOne>, ab: (_: Effect.Effect<string, never, ServiceOne>) => Effect.Effect<string, never, never>): Effect.Effect<string, never, never> (+21 overloads)pipe(import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const provide: <ServiceOne>(context: Context.Context<ServiceOne>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, ServiceOne>> (+9 overloads)

Provides necessary dependencies to an effect, removing its environmental requirements.

Details

This function allows you to supply the required environment for an effect. The environment can be provided in the form of one or more Layers, a Context, a Runtime, or a ManagedRuntime. Once the environment is provided, the effect can run without requiring external dependencies.

You can compose layers to create a modular and reusable way of setting up the environment for effects. For example, layers can be used to configure databases, logging services, or any other required dependencies.

Example

import { Context, Effect, Layer } from "effect"

class Database extends Context.Tag("Database")<
  Database,
  { readonly query: (sql: string) => Effect.Effect<Array<unknown>> }
>() {}

const DatabaseLive = Layer.succeed(
  Database,
  {
    // Simulate a database query
    query: (sql: string) => Effect.log(`Executing query: ${sql}`).pipe(Effect.as([]))
  }
)

//      ┌─── Effect<unknown[], never, Database>
//      ▼
const program = Effect.gen(function*() {
  const database = yield* Database
  const result = yield* database.query("SELECT * FROM users")
  return result
})

//      ┌─── Effect<unknown[], never, never>
//      ▼
const runnable = Effect.provide(program, DatabaseLive)

Effect.runPromise(runnable).then(console.log)
// Output:
// timestamp=... level=INFO fiber=#0 message="Executing query: SELECT * FROM users"
// []
@see{@link provideService} for providing a service to an effect.@since2.0.0@categoryContext
provide
(const context: Context.Context<ServiceOne>context));
}) } }));

As we get into 5+ services territory the code for specifying dependencies and adding them all to a reusable context starts to smell like unnecessary boilerplate.

const serviceOne = yield* ServiceOne;
const serviceTwo = yield* ServiceTwo;
const serviceThree = yield* ServiceThree;
const serviceFour = yield* ServiceFour;
const serviceFive = yield* ServiceFive;
const context = e.pipe(
  e.Context.empty(),
  e.Context.add(ServiceOne, serviceOne),
  e.Context.add(ServiceTwo, serviceTwo),
  e.Context.add(ServiceThree, serviceThree),
  e.Context.add(ServiceFour, serviceFour),
  e.Context.add(ServiceFive, serviceFive)
)

This is what we’re calling state-of-the-art TypeScript in 2026?

5. Confusing Naming Conventions

Lastly, I’ve always been bothered by Effect’s use of the term “service”. To me, “service” alludes to some outside entity which my code can invoke via some network call, but in Effect a service is basically a singleton instance which encapsulates some common internal logic. “Tag” is also confusing compared to a well-known term like “interface”. I’m aware that Effect is following conventions established by other DI frameworks like ZIO or Angular, but from my POV defaults should be sensible and names shouldn’t be surprising.

Effective Modules

All these headaches had me searching for ways to make Effect feel more self-explanatory and intuitive. Effect seems to be creating the right set of foundational primitives for building production-grade TypeScript, so it seems worthwhile to invest in making these primitives feel good to work with.

Enough experimentation eventually led to something packageable as its own library. I’m calling it Effective Modules. Perhaps some of these ideas / patterns might make their way into Effect core and / or the canonical docs / examples at some point? Let me know what you think.

Effective?

I’m coining the word “Effective” here to mean idiomatic, elegant Effect code. “Effective” is to Effect as “Pythonic” is to Python. Not to be confused with “Effectful”.

Modules?

Modules is my word of choice over “tag” or “service”. Module typically refers to some group of encapsulated code which is internal to a project. Module also implies a grouping of related functionality, and when Uncle Bob first described the Dependency Inversion Principle he used “class” and “module” interchangeably when referring to code building blocks that depend on each other. The wiki article on DIP does this too.

I confess to substituting one overloaded term for another with “modules”

Despite the phrase’s frequent use, the commonality here is that “module” in all these cases refers to code that’s internal, encapsulated, and logically grouped under a namespace. To me, that’s a more sensible fit than “service”. I’d love to see a rebuttal.

Module Declaration

With Effective Modules, we return to the plain interface.

Before

class class UsersUsers extends import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const Tag: <"Users">(id: "Users") => <Self, Shape>() => Context.TagClass<Self, "Users", Shape>
@example
import * as assert from "node:assert"
import { Context, Layer } from "effect"

class MyTag extends Context.Tag("MyTag")<
 MyTag,
 { readonly myNum: number }
>() {
 static Live = Layer.succeed(this, { myNum: 108 })
}
@since2.0.0@categoryconstructors
Tag
("Users")<
class UsersUsers, {
function createUser(username: string, signature: string): Effect.Effect<{
    token: string;
}, BadSignature>
createUser
(
username: stringusername: string, signature: stringsignature: string ): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0@categoryModels@since2.0.0
Effect
<{token: stringtoken: string}, class BadSignatureBadSignature>;
function authenticate(username: string, signature: string): Effect.Effect<{
    token: string;
}, BadSignature>
authenticate
(
username: stringusername: string, signature: stringsignature: string ): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.interface Effect<out A, out E = never, out R = never>

The Effect interface defines a value that describes a workflow or job, which can succeed or fail.

Details

The Effect interface represents a computation that can model a workflow involving various types of operations, such as synchronous, asynchronous, concurrent, and parallel interactions. It operates within a context of type R, and the result can either be a success with a value of type A or a failure with an error of type E. The Effect is designed to handle complex interactions with external resources, offering advanced features such as fiber-based concurrency, scheduling, interruption handling, and scalability. This makes it suitable for tasks that require fine-grained control over concurrency and error management.

To execute an Effect value, you need a Runtime, which provides the environment necessary to run and manage the computation.

@since2.0.0@categoryModels@since2.0.0
Effect
<{token: stringtoken: string}, class BadSignatureBadSignature>;
} >() {}

After

interface IUsers {
  
IUsers.createUser(username: string, signature: string): Effect.fn.Return<{
    token: string;
}, BadSignature>
createUser
(
username: stringusername: string, signature: stringsignature: string ): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.fn.type fn.Return<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>
@since3.19.0@categoryModels
Return
<{token: stringtoken: string}, class BadSignatureBadSignature>;
IUsers.authenticate(username: string, signature: string): Effect.fn.Return<{
    token: string;
}, BadSignature>
authenticate
(
username: stringusername: string, signature: stringsignature: string ): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.fn.type fn.Return<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>
@since3.19.0@categoryModels
Return
<{token: stringtoken: string}, class BadSignatureBadSignature>;
}

Tags (Services in v4) are created by mapping each module name to an interface.

import { function interfaces<ModuleKeysEnum extends string, Interfaces extends { [moduleKey in ModuleKeysEnum]: any; }>(moduleKeysEnum: StringEnum<ModuleKeysEnum>): { [moduleKey in ModuleKeysEnum]: Tag<moduleKey, Interfaces[moduleKey]>; }interfaces } from "effective-modules";

export enum enum ModulesModules {
  function (enum member) Modules.Users = "Users"Users = "Users",
  function (enum member) Modules.Todos = "Todos"Todos = "Todos",
  function (enum member) Modules.Database = "Database"Database = "Database"
}

export const 
const modules: {
    Users: Tag<Modules.Users, IUsers>;
    Todos: Tag<Modules.Todos, ITodos>;
    Database: Tag<Modules.Database, IDatabase>;
}
modules
=
interfaces<Modules, {
    Users: IUsers;
    Todos: ITodos;
    Database: IDatabase;
}>(moduleKeysEnum: StringEnum<Modules>): {
    Users: Tag<Modules.Users, IUsers>;
    Todos: Tag<Modules.Todos, ITodos>;
    Database: Tag<Modules.Database, IDatabase>;
}
interfaces
<enum ModulesModules, {
type Users: IUsersUsers: IUsers; type Todos: ITodosTodos: ITodos; type Database: IDatabaseDatabase: IDatabase; }>(enum ModulesModules);
const modules: {
    Users: Tag<Modules.Users, IUsers>;
    Todos: Tag<Modules.Todos, ITodos>;
    Database: Tag<Modules.Database, IDatabase>;
}
modules
;

The string enum members are used as the Tags’ Self and key types, bypassing the need for the cluttered class syntax mentioned earlier since string enum members are similarly both a runtime value and a type. This method of creating Tags is also a more correct means of ensuring uniqueness since string enum members are nominal types, meaning TypeScript will treat two Effective Modules with the same name as distinct. No fancy private property tricks necessary.

enum enum ModuleSetOneModuleSetOne {
  function (enum member) ModuleSetOne.Users = "Users"Users = "Users",
  function (enum member) ModuleSetOne.Database = "Database"Database = "Database"
}

const 
const moduleSetOne: {
    Users: Tag<ModuleSetOne.Users, {}>;
    Database: Tag<ModuleSetOne.Database, {}>;
}
moduleSetOne
=
interfaces<ModuleSetOne, {
    Users: {};
    Database: {};
}>(moduleKeysEnum: StringEnum<ModuleSetOne>): {
    Users: Tag<ModuleSetOne.Users, {}>;
    Database: Tag<ModuleSetOne.Database, {}>;
}
interfaces
<enum ModuleSetOneModuleSetOne, {
type Users: {}Users: {}; type Database: {}Database: {}; }>(enum ModuleSetOneModuleSetOne); enum enum ModuleSetTwoModuleSetTwo { function (enum member) ModuleSetTwo.Users = "Users"Users = "Users", function (enum member) ModuleSetTwo.Database = "Database"Database = "Database" } const
const moduleSetTwo: {
    Users: Tag<ModuleSetTwo.Users, {}>;
    Database: Tag<ModuleSetTwo.Database, {}>;
}
moduleSetTwo
=
interfaces<ModuleSetTwo, {
    Users: {};
    Database: {};
}>(moduleKeysEnum: StringEnum<ModuleSetTwo>): {
    Users: Tag<ModuleSetTwo.Users, {}>;
    Database: Tag<ModuleSetTwo.Database, {}>;
}
interfaces
<enum ModuleSetTwoModuleSetTwo, {
type Users: {}Users: {}; type Database: {}Database: {}; }>(enum ModuleSetTwoModuleSetTwo); function* function program(): Effect.fn.Return<void, never, ModuleSetTwo.Users>program(): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.fn.type fn.Return<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>
@since3.19.0@categoryModels
Return
<void, never, enum ModuleSetTwoModuleSetTwo.function (enum member) ModuleSetTwo.Users = "Users"Users> {
yield*
const moduleSetTwo: {
    Users: Tag<ModuleSetTwo.Users, {}>;
    Database: Tag<ModuleSetTwo.Database, {}>;
}
moduleSetTwo
.type Users: Tag<ModuleSetTwo.Users, {}>Users;
// A compiler error, as there should be. yield* moduleSetOne.Users;
Type 'YieldWrap<Tag<ModuleSetOne.Users, {}>>' is not assignable to type 'YieldWrap<Effect<any, never, ModuleSetTwo.Users>>'. Type 'Tag<ModuleSetOne.Users, {}>' is not assignable to type 'Effect<any, never, ModuleSetTwo.Users>'. Types of property '[EffectTypeId]' are incompatible. Type 'VarianceStruct<{}, never, ModuleSetOne.Users>' is not assignable to type 'VarianceStruct<any, never, ModuleSetTwo.Users>'. Type 'ModuleSetOne.Users' is not assignable to type 'ModuleSetTwo.Users'.
}

We’ll dive into this a bit more later, but notice how we’ve managed to get an error at a specific line rather than the entire generator function being marked as incorrect. This is achieved by explicitly typing the return of the generator function using Effect.fn.Return rather than using the prescribed Effect.gen or Effect.fn + anon generator function.

Module Implementation

In Effective Modules, you create a class which implements an interface.

Before

export const const TodosLive: Layer.Layer<Modules.Todos, never, Modules.Users | Modules.Database>TodosLive = import LayerLayer.const effect: <Modules.Todos, ITodos, never, Modules.Users | Modules.Database>(tag: Tag<Modules.Todos, ITodos>, effect: Effect.Effect<ITodos, never, Modules.Users | Modules.Database>) => Layer.Layer<Modules.Todos, never, Modules.Users | Modules.Database> (+1 overload)

Constructs a layer from the specified effect.

@since2.0.0@categoryconstructors
effect
(
const Todos: Tag<Modules.Todos, ITodos>Todos, import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const gen: <YieldWrap<Tag<Modules.Database, IDatabase>> | YieldWrap<Tag<Modules.Users, IUsers>>, {
    getTasks: (token: string) => Effect.Effect<{
        id: string;
        task: string;
    }[], InvalidToken, never>;
    createTask: (token: string, task: string) => Effect.Effect<{
        id: string;
        task: string;
    }, InvalidToken, never>;
    completeTask: (token: string, id: string) => Effect.Effect<undefined, InvalidToken | NoSuchTask, never>;
}>(f: (resume: Effect.Adapter) => Generator<...>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"

const addServiceCharge = (amount: number) => amount + 1

const applyDiscount = (
  total: number,
  discountRate: number
): Effect.Effect<number, Error> =>
  discountRate === 0
    ? Effect.fail(new Error("Discount rate cannot be zero"))
    : Effect.succeed(total - (total * discountRate) / 100)

const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))

const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))

export const program = Effect.gen(function* () {
  const transactionAmount = yield* fetchTransactionAmount
  const discountRate = yield* fetchDiscountRate
  const discountedAmount = yield* applyDiscount(
    transactionAmount,
    discountRate
  )
  const finalAmount = addServiceCharge(discountedAmount)
  return `Final amount to charge: ${finalAmount}`
})
@since2.0.0@categoryCreating Effects
gen
(function*() {
const const users: IUsersusers = yield* const Users: Tag<Modules.Users, IUsers>Users; const const db: IDatabasedb = yield* const Database: Tag<Modules.Database, IDatabase>Database; const const TASKS_TABLE: "tasks"TASKS_TABLE = "tasks"; const const validateTokenOrError: (token: string) => Effect.Effect<string, InvalidToken, never>validateTokenOrError = import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const fn: <YieldWrap<Effect.Effect<never, InvalidToken, never>> | YieldWrap<Effect.Effect<any, never, never>>, string, [token: string]>(body: (token: string) => Generator<YieldWrap<Effect.Effect<never, InvalidToken, never>> | YieldWrap<Effect.Effect<any, never, never>>, string, never>) => (token: string) => Effect.Effect<string, InvalidToken, never> (+20 overloads)fn(function*(token: stringtoken: string) {
const
const maybeUsername: Option.Option<{
    username: string;
}>
maybeUsername
= yield* const users: IUsersusers.
IUsers.validateToken(token: string): Effect.fn.Return<Option.Option<{
    username: string;
}>>
validateToken
(token: stringtoken);
if (import Option
@since2.0.0@categorymodels@since2.0.0@categoryModels
Option
.
const isNone: <{
    username: string;
}>(self: Option.Option<{
    username: string;
}>) => self is Option.None<{
    username: string;
}>

Checks whether an Option represents the absence of a value (None).

@example
import { Option } from "effect"

console.log(Option.isNone(Option.some(1)))
// Output: false

console.log(Option.isNone(Option.none()))
// Output: true
@see{@link isSome} for the opposite check.@categoryGuards@since2.0.0
isNone
(
const maybeUsername: Option.Option<{
    username: string;
}>
maybeUsername
)) {
return yield* new constructor InvalidToken<{}>(args: void): InvalidTokenInvalidToken(); } return
const maybeUsername: Option.Some<{
    username: string;
}>
maybeUsername
.
Some<{ username: string; }>.value: {
    username: string;
}
value
.username: stringusername;
}); return {
getTasks: (token: string) => Effect.Effect<{
    id: string;
    task: string;
}[], InvalidToken, never>
getTasks
: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const fn: <YieldWrap<Effect.Effect<string, InvalidToken, never>> | YieldWrap<Effect.Effect<{
    key: string;
    value: string;
}[], never, never>>, {
    id: string;
    task: string;
}[], [token: string]>(body: (token: string) => Generator<YieldWrap<Effect.Effect<string, InvalidToken, never>> | YieldWrap<Effect.Effect<{
    key: string;
    value: string;
}[], never, never>>, {
    id: string;
    task: string;
}[], never>) => (token: string) => Effect.Effect<{
    id: string;
    task: string;
}[], InvalidToken, never> (+20 overloads)
fn
(function*(token: stringtoken) {
const const username: stringusername = yield* const validateTokenOrError: (token: string) => Effect.Effect<string, InvalidToken, never>validateTokenOrError(token: stringtoken); const
const items: {
    key: string;
    value: string;
}[]
items
= yield* const db: IDatabasedb.
IDatabase.getAll(table: string, username: string): Effect.Effect<{
    key: string;
    value: string;
}[]>
getAll
(const TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername);
return
const items: {
    key: string;
    value: string;
}[]
items
.
Array<{ key: string; value: string; }>.map<{
    id: string;
    task: string;
}>(callbackfn: (value: {
    key: string;
    value: string;
}, index: number, array: {
    key: string;
    value: string;
}[]) => {
    id: string;
    task: string;
}, thisArg?: any): {
    id: string;
    task: string;
}[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map
(({key: stringkey, value: stringvalue}) => ({id: stringid: key: stringkey, task: stringtask: value: stringvalue}));
}),
createTask: (token: string, task: string) => Effect.Effect<{
    id: string;
    task: string;
}, InvalidToken, never>
createTask
: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const fn: <YieldWrap<Effect.Effect<any, never, never>> | YieldWrap<Effect.Effect<string, InvalidToken, never>>, {
    id: string;
    task: string;
}, [token: string, task: string]>(body: (token: string, task: string) => Generator<YieldWrap<Effect.Effect<any, never, never>> | YieldWrap<Effect.Effect<string, InvalidToken, never>>, {
    id: string;
    task: string;
}, never>) => (token: string, task: string) => Effect.Effect<{
    id: string;
    task: string;
}, InvalidToken, never> (+20 overloads)
fn
(function*(token: stringtoken, task: stringtask) {
const const username: stringusername = yield* const validateTokenOrError: (token: string) => Effect.Effect<string, InvalidToken, never>validateTokenOrError(token: stringtoken); const { const key: stringkey } = yield* const db: IDatabasedb.
IDatabase.set(table: string, username: string, value: string, key?: string): Effect.fn.Return<{
    key: string;
}>
set
(const TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, task: stringtask);
return {id: stringid: const key: stringkey, task: stringtask}; }), completeTask: (token: string, id: string) => Effect.Effect<undefined, InvalidToken | NoSuchTask, never>completeTask: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const fn: <YieldWrap<Effect.Effect<any, never, never>> | YieldWrap<Effect.Effect<string, InvalidToken, never>> | YieldWrap<Effect.Effect<never, NoSuchTask, never>>, undefined, [token: string, id: string]>(body: (token: string, id: string) => Generator<YieldWrap<Effect.Effect<any, never, never>> | YieldWrap<Effect.Effect<string, InvalidToken, never>> | YieldWrap<Effect.Effect<never, NoSuchTask, never>>, undefined, never>) => (token: string, id: string) => Effect.Effect<...> (+20 overloads)fn(function* (token: stringtoken, id: stringid) {
const const username: stringusername = yield* const validateTokenOrError: (token: string) => Effect.Effect<string, InvalidToken, never>validateTokenOrError(token: stringtoken); const const exists: booleanexists = import Option
@since2.0.0@categorymodels@since2.0.0@categoryModels
Option
.const isSome: <string>(self: Option.Option<string>) => self is Option.Some<string>

Checks whether an Option contains a value (Some).

@example
import { Option } from "effect"

console.log(Option.isSome(Option.some(1)))
// Output: true

console.log(Option.isSome(Option.none()))
// Output: false
@see{@link isNone} for the opposite check.@categoryGuards@since2.0.0
isSome
(yield* const db: IDatabasedb.IDatabase.get(table: string, username: string, key: string): Effect.fn.Return<Option.Option<string>>get(const TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, id: stringid));
if (!const exists: booleanexists) return yield* new constructor NoSuchTask<{}>(args: void): NoSuchTaskNoSuchTask(); yield* const db: IDatabasedb.IDatabase.delete(table: string, username: string, key: string): Effect.fn.Return<void>delete(const TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, id: stringid); }) }; }) )
const TodosLive: Layer.Layer<Modules.Todos, never, Modules.Users | Modules.Database>
TodosLive

After

import { const implementing: <Module extends Tag<any, any>>(module: Module) => ModuleSuperClass<Module, None, None>implementing } from "effective-modules";

export class class TodosImplTodosImpl extends implementing<Tag<Modules.Todos, ITodos>>(module: Tag<Modules.Todos, ITodos>): ModuleSuperClass<Tag<Modules.Todos, ITodos>, None, None>implementing(const Todos: Tag<Modules.Todos, ITodos>Todos).uses: <Tag<Modules.Database, IDatabase>, [Tag<Modules.Users, IUsers>]>(first: Tag<Modules.Database, IDatabase>, others_0: Tag<Modules.Users, IUsers>) => ModuleSuperClass<Tag<Modules.Todos, ITodos>, Some<[Tag<Modules.Database, IDatabase>, Tag<Modules.Users, IUsers>]>, None>uses(const Database: Tag<Modules.Database, IDatabase>Database, const Users: Tag<Modules.Users, IUsers>Users) implements ITodos {
  private static readonly TodosImpl.TASKS_TABLE: "tasks"TASKS_TABLE = "tasks";

  *
TodosImpl.getTasks(token: string): Effect.fn.Return<{
    task: string;
    id: string;
}[], InvalidToken>
getTasks
(token: stringtoken: string): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.fn.type fn.Return<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>
@since3.19.0@categoryModels
Return
<{ task: stringtask: string; id: stringid: string; }[], class InvalidTokenInvalidToken> {
const const username: stringusername = yield* this.TodosImpl.validateTokenOrError(token: string): Effect.fn.Return<string, InvalidToken>validateTokenOrError(token: stringtoken); const
const items: {
    key: string;
    value: string;
}[]
items
= yield* this.
dependencies: {
    readonly Database: IDatabase;
    readonly Users: IUsers;
}
dependencies
.type Database: IDatabaseDatabase.
IDatabase.getAll(table: string, username: string): Effect.fn.Return<{
    key: string;
    value: string;
}[]>
getAll
(
class TodosImplTodosImpl.TodosImpl.TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername ); return
const items: {
    key: string;
    value: string;
}[]
items
.
Array<{ key: string; value: string; }>.map<{
    id: string;
    task: string;
}>(callbackfn: (value: {
    key: string;
    value: string;
}, index: number, array: {
    key: string;
    value: string;
}[]) => {
    id: string;
    task: string;
}, thisArg?: any): {
    id: string;
    task: string;
}[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map
(({key: stringkey, value: stringvalue}) => ({id: stringid: key: stringkey, task: stringtask: value: stringvalue}));
} *
TodosImpl.createTask(token: string, task: string): Effect.fn.Return<{
    task: string;
    id: string;
}, InvalidToken>
createTask
(token: stringtoken: string, task: stringtask: string): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.fn.type fn.Return<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>
@since3.19.0@categoryModels
Return
<{ task: stringtask: string; id: stringid: string; }, class InvalidTokenInvalidToken> {
const const username: stringusername = yield* this.TodosImpl.validateTokenOrError(token: string): Effect.fn.Return<string, InvalidToken>validateTokenOrError(token: stringtoken); const { const key: stringkey } = yield* this.
dependencies: {
    readonly Database: IDatabase;
    readonly Users: IUsers;
}
dependencies
.type Database: IDatabaseDatabase.
IDatabase.set(table: string, username: string, value: string, key?: string): Effect.fn.Return<{
    key: string;
}>
set
(
class TodosImplTodosImpl.TodosImpl.TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, task: stringtask ); return {id: stringid: const key: stringkey, task: stringtask}; } *TodosImpl.completeTask(token: string, id: string): Effect.fn.Return<void, NoSuchTask | InvalidToken>completeTask(token: stringtoken: string, id: stringid: string): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.fn.type fn.Return<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>
@since3.19.0@categoryModels
Return
<void, class NoSuchTaskNoSuchTask | class InvalidTokenInvalidToken> {
const const username: stringusername = yield* this.TodosImpl.validateTokenOrError(token: string): Effect.fn.Return<string, InvalidToken>validateTokenOrError(token: stringtoken); const const exists: booleanexists = import Option
@since2.0.0@categorymodels@since2.0.0@categoryModels
Option
.const isSome: <string>(self: Option.Option<string>) => self is Option.Some<string>

Checks whether an Option contains a value (Some).

@example
import { Option } from "effect"

console.log(Option.isSome(Option.some(1)))
// Output: true

console.log(Option.isSome(Option.none()))
// Output: false
@see{@link isNone} for the opposite check.@categoryGuards@since2.0.0
isSome
(yield* this.
dependencies: {
    readonly Database: IDatabase;
    readonly Users: IUsers;
}
dependencies
.type Database: IDatabaseDatabase.IDatabase.get(table: string, username: string, key: string): Effect.fn.Return<Option.Option<string>>get(
class TodosImplTodosImpl.TodosImpl.TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, id: stringid )); if (!const exists: booleanexists) return yield* new constructor NoSuchTask<{}>(args: void): NoSuchTaskNoSuchTask(); yield* this.
dependencies: {
    readonly Database: IDatabase;
    readonly Users: IUsers;
}
dependencies
.type Database: IDatabaseDatabase.IDatabase.delete(table: string, username: string, key: string): Effect.fn.Return<void>delete(
class TodosImplTodosImpl.TodosImpl.TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, id: stringid ); } private *TodosImpl.validateTokenOrError(token: string): Effect.fn.Return<string, InvalidToken>validateTokenOrError(token: stringtoken: string): import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.fn.type fn.Return<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>
@since3.19.0@categoryModels
Return
<string, class InvalidTokenInvalidToken> {
const
const maybeUsername: Option.Option<{
    username: string;
}>
maybeUsername
= yield* this.
dependencies: {
    readonly Database: IDatabase;
    readonly Users: IUsers;
}
dependencies
.type Users: IUsersUsers.
IUsers.validateToken(token: string): Effect.fn.Return<Option.Option<{
    username: string;
}>>
validateToken
(token: stringtoken);
if (import Option
@since2.0.0@categorymodels@since2.0.0@categoryModels
Option
.
const isNone: <{
    username: string;
}>(self: Option.Option<{
    username: string;
}>) => self is Option.None<{
    username: string;
}>

Checks whether an Option represents the absence of a value (None).

@example
import { Option } from "effect"

console.log(Option.isNone(Option.some(1)))
// Output: false

console.log(Option.isNone(Option.none()))
// Output: true
@see{@link isSome} for the opposite check.@categoryGuards@since2.0.0
isNone
(
const maybeUsername: Option.Option<{
    username: string;
}>
maybeUsername
)) {
return yield* new constructor InvalidToken<{}>(args: void): InvalidTokenInvalidToken(); } return
const maybeUsername: Option.Some<{
    username: string;
}>
maybeUsername
.
Some<{ username: string; }>.value: {
    username: string;
}
value
.username: stringusername;
} } class TodosImplTodosImpl.
type Layer: Layer.Layer<Modules.Todos, never, Modules.Users | Modules.Database>
Layer
;

The superclass implementing(modules.Todos) created the dependencies structure automatically, and IDE autocomplete for classes implementing an interface will generate method stubs so you don’t need to type out the method signatures manually. Generator methods are used directly rather than wrapping in Effect.fn because this allows us to cleanly access this.dependencies and this.context.

Precise Error Location

If you get nothing else from this article take this away: explicitly annotating your generator functions with a return type of Effect.fn.Return<A, E, R> will enable exact error locations. This type alias ships with Effect. It represents an Effect Generator. Effective Modules provides a EffectGen<A, E, R> type alias for convenience.

Here’s the same ensureLoggedIn example from earlier but now we have a clear, targeted compiler error on the resolveAccessToken call line.

Let’s see this in action in our Todo example where we’ll yield an unexpected error from the validateTokenOrError method.

Before

export const const TodosLive: Layer.Layer<Modules.Todos, never, Modules.Users | Modules.Database>TodosLive = import LayerLayer.const effect: <Modules.Todos, ITodos, never, Modules.Users | Modules.Database>(tag: Tag<Modules.Todos, ITodos>, effect: Effect.Effect<ITodos, never, Modules.Users | Modules.Database>) => Layer.Layer<Modules.Todos, never, Modules.Users | Modules.Database> (+1 overload)

Constructs a layer from the specified effect.

@since2.0.0@categoryconstructors
effect
(
const Todos: Tag<Modules.Todos, ITodos>Todos, Effect.gen(function*() {
Argument of type 'Effect<{ getTasks: (token: string) => Effect<{ id: string; task: string; }[], BadSignature | InvalidToken, never>; createTask: (token: string, task: string) => Effect<...>; completeTask: (token: string, id: string) => Effect<...>; }, never, Modules.Users | Modules.Database>' is not assignable to parameter of type 'Effect<ITodos, never, Modules.Users | Modules.Database>'. Type '{ getTasks: (token: string) => Effect.Effect<{ id: string; task: string; }[], BadSignature | InvalidToken, never>; createTask: (token: string, task: string) => Effect.Effect<...>; completeTask: (token: string, id: string) => Effect.Effect<...>; }' is not assignable to type 'ITodos'. The types returned by 'getTasks(...)' are incompatible between these types. Type 'Effect<{ id: string; task: string; }[], BadSignature | InvalidToken, never>' is not assignable to type 'Effect<{ task: string; id: string; }[], InvalidToken, never>'. Type 'BadSignature | InvalidToken' is not assignable to type 'InvalidToken'. Type 'BadSignature' is not assignable to type 'InvalidToken'. Types of property '_tag' are incompatible. Type '"BadSignature"' is not assignable to type '"InvalidToken"'.
const const users: IUsersusers = yield* const Users: Tag<Modules.Users, IUsers>Users; const const db: IDatabasedb = yield* const Database: Tag<Modules.Database, IDatabase>Database; const const TASKS_TABLE: "tasks"TASKS_TABLE = "tasks"; const const validateTokenOrError: (token: string) => Effect.Effect<string, InvalidToken | BadSignature, never>validateTokenOrError = import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const fn: <YieldWrap<Effect.Effect<never, InvalidToken, never>> | YieldWrap<Effect.Effect<never, BadSignature, never>> | YieldWrap<Effect.Effect<any, never, never>>, string, [token: string]>(body: (token: string) => Generator<YieldWrap<Effect.Effect<never, InvalidToken, never>> | YieldWrap<Effect.Effect<never, BadSignature, never>> | YieldWrap<Effect.Effect<any, never, never>>, string, never>) => (token: string) => Effect.Effect<...> (+20 overloads)fn(function*(token: stringtoken: string) {
const
const maybeUsername: Option.Option<{
    username: string;
}>
maybeUsername
= yield* const users: IUsersusers.
IUsers.validateToken(token: string): Effect.fn.Return<Option.Option<{
    username: string;
}>>
validateToken
(token: stringtoken);
if (import Option
@since2.0.0@categorymodels@since2.0.0@categoryModels
Option
.
const isNone: <{
    username: string;
}>(self: Option.Option<{
    username: string;
}>) => self is Option.None<{
    username: string;
}>

Checks whether an Option represents the absence of a value (None).

@example
import { Option } from "effect"

console.log(Option.isNone(Option.some(1)))
// Output: false

console.log(Option.isNone(Option.none()))
// Output: true
@see{@link isSome} for the opposite check.@categoryGuards@since2.0.0
isNone
(
const maybeUsername: Option.Option<{
    username: string;
}>
maybeUsername
)) {
return yield* new constructor InvalidToken<{}>(args: void): InvalidTokenInvalidToken(); } if (token: stringtoken.String.length: number

Returns the length of a String object.

length
) {
return yield* new constructor BadSignature<{}>(args: void): BadSignatureBadSignature(); } return
const maybeUsername: Option.Some<{
    username: string;
}>
maybeUsername
.
Some<{ username: string; }>.value: {
    username: string;
}
value
.username: stringusername;
}); return {
getTasks: (token: string) => Effect.Effect<{
    id: string;
    task: string;
}[], InvalidToken | BadSignature, never>
getTasks
: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const fn: <YieldWrap<Effect.Effect<string, InvalidToken | BadSignature, never>> | YieldWrap<Effect.Effect<{
    key: string;
    value: string;
}[], never, never>>, {
    id: string;
    task: string;
}[], [token: string]>(body: (token: string) => Generator<YieldWrap<Effect.Effect<string, InvalidToken | BadSignature, never>> | YieldWrap<Effect.Effect<{
    key: string;
    value: string;
}[], never, never>>, {
    id: string;
    task: string;
}[], never>) => (token: string) => Effect.Effect<...> (+20 overloads)
fn
(function*(token: stringtoken) {
const const username: stringusername = yield* const validateTokenOrError: (token: string) => Effect.Effect<string, InvalidToken | BadSignature, never>validateTokenOrError(token: stringtoken); const
const items: {
    key: string;
    value: string;
}[]
items
= yield* const db: IDatabasedb.
IDatabase.getAll(table: string, username: string): Effect.Effect<{
    key: string;
    value: string;
}[]>
getAll
(const TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername);
return
const items: {
    key: string;
    value: string;
}[]
items
.
Array<{ key: string; value: string; }>.map<{
    id: string;
    task: string;
}>(callbackfn: (value: {
    key: string;
    value: string;
}, index: number, array: {
    key: string;
    value: string;
}[]) => {
    id: string;
    task: string;
}, thisArg?: any): {
    id: string;
    task: string;
}[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map
(({key: stringkey, value: stringvalue}) => ({id: stringid: key: stringkey, task: stringtask: value: stringvalue}));
}),
createTask: (token: string, task: string) => Effect.Effect<{
    id: string;
    task: string;
}, InvalidToken | BadSignature, never>
createTask
: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const fn: <YieldWrap<Effect.Effect<any, never, never>> | YieldWrap<Effect.Effect<string, InvalidToken | BadSignature, never>>, {
    id: string;
    task: string;
}, [token: string, task: string]>(body: (token: string, task: string) => Generator<YieldWrap<Effect.Effect<any, never, never>> | YieldWrap<Effect.Effect<string, InvalidToken | BadSignature, never>>, {
    id: string;
    task: string;
}, never>) => (token: string, task: string) => Effect.Effect<...> (+20 overloads)
fn
(function*(token: stringtoken, task: stringtask) {
const const username: stringusername = yield* const validateTokenOrError: (token: string) => Effect.Effect<string, InvalidToken | BadSignature, never>validateTokenOrError(token: stringtoken); const { const key: stringkey } = yield* const db: IDatabasedb.
IDatabase.set(table: string, username: string, value: string, key?: string): Effect.fn.Return<{
    key: string;
}>
set
(const TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, task: stringtask);
return {id: stringid: const key: stringkey, task: stringtask}; }), completeTask: (token: string, id: string) => Effect.Effect<undefined, InvalidToken | BadSignature | NoSuchTask, never>completeTask: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const fn: <YieldWrap<Effect.Effect<any, never, never>> | YieldWrap<Effect.Effect<string, InvalidToken | BadSignature, never>> | YieldWrap<Effect.Effect<never, NoSuchTask, never>>, undefined, [token: string, id: string]>(body: (token: string, id: string) => Generator<YieldWrap<Effect.Effect<any, never, never>> | YieldWrap<Effect.Effect<string, InvalidToken | BadSignature, never>> | YieldWrap<...>, undefined, never>) => (token: string, id: string) => Effect.Effect<...> (+20 overloads)fn(function* (token: stringtoken, id: stringid) {
const const username: stringusername = yield* const validateTokenOrError: (token: string) => Effect.Effect<string, InvalidToken | BadSignature, never>validateTokenOrError(token: stringtoken); const const exists: booleanexists = import Option
@since2.0.0@categorymodels@since2.0.0@categoryModels
Option
.const isSome: <string>(self: Option.Option<string>) => self is Option.Some<string>

Checks whether an Option contains a value (Some).

@example
import { Option } from "effect"

console.log(Option.isSome(Option.some(1)))
// Output: true

console.log(Option.isSome(Option.none()))
// Output: false
@see{@link isNone} for the opposite check.@categoryGuards@since2.0.0
isSome
(yield* const db: IDatabasedb.IDatabase.get(table: string, username: string, key: string): Effect.fn.Return<Option.Option<string>>get(const TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, id: stringid));
if (!const exists: booleanexists) return yield* new constructor NoSuchTask<{}>(args: void): NoSuchTaskNoSuchTask(); yield* const db: IDatabasedb.IDatabase.delete(table: string, username: string, key: string): Effect.fn.Return<void>delete(const TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, id: stringid); }) }; }) )

After

import { const implementing: <Module extends Tag<any, any>>(module: Module) => ModuleSuperClass<Module, None, None>implementing, type type EffectGen<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>EffectGen } from "effective-modules";

export class class TodosImplTodosImpl extends implementing<Tag<Modules.Todos, ITodos>>(module: Tag<Modules.Todos, ITodos>): ModuleSuperClass<Tag<Modules.Todos, ITodos>, None, None>implementing(const Todos: Tag<Modules.Todos, ITodos>Todos).uses: <Tag<Modules.Database, IDatabase>, [Tag<Modules.Users, IUsers>]>(first: Tag<Modules.Database, IDatabase>, others_0: Tag<Modules.Users, IUsers>) => ModuleSuperClass<Tag<Modules.Todos, ITodos>, Some<[Tag<Modules.Database, IDatabase>, Tag<Modules.Users, IUsers>]>, None>uses(const Database: Tag<Modules.Database, IDatabase>Database, const Users: Tag<Modules.Users, IUsers>Users) implements ITodos {
  private static readonly TodosImpl.TASKS_TABLE: "tasks"TASKS_TABLE = "tasks";

  *
TodosImpl.getTasks(token: string): EffectGen<{
    task: string;
    id: string;
}[], InvalidToken>
getTasks
(token: stringtoken: string): type EffectGen<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>EffectGen<{ task: stringtask: string; id: stringid: string; }[], class InvalidTokenInvalidToken> {
const const username: stringusername = yield* this.TodosImpl.validateTokenOrError(token: string): EffectGen<string, InvalidToken>validateTokenOrError(token: stringtoken); const
const items: {
    key: string;
    value: string;
}[]
items
= yield* this.
dependencies: {
    readonly Database: IDatabase;
    readonly Users: IUsers;
}
dependencies
.type Database: IDatabaseDatabase.
IDatabase.getAll(table: string, username: string): Effect.fn.Return<{
    key: string;
    value: string;
}[]>
getAll
(
class TodosImplTodosImpl.TodosImpl.TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername ); return
const items: {
    key: string;
    value: string;
}[]
items
.
Array<{ key: string; value: string; }>.map<{
    id: string;
    task: string;
}>(callbackfn: (value: {
    key: string;
    value: string;
}, index: number, array: {
    key: string;
    value: string;
}[]) => {
    id: string;
    task: string;
}, thisArg?: any): {
    id: string;
    task: string;
}[]

Calls a defined callback function on each element of an array, and returns an array that contains the results.

@paramcallbackfn A function that accepts up to three arguments. The map method calls the callbackfn function one time for each element in the array.@paramthisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
map
(({key: stringkey, value: stringvalue}) => ({id: stringid: key: stringkey, task: stringtask: value: stringvalue}));
} *
TodosImpl.createTask(token: string, task: string): EffectGen<{
    task: string;
    id: string;
}, InvalidToken>
createTask
(token: stringtoken: string, task: stringtask: string): type EffectGen<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>EffectGen<{ task: stringtask: string; id: stringid: string; }, class InvalidTokenInvalidToken> {
const const username: stringusername = yield* this.TodosImpl.validateTokenOrError(token: string): EffectGen<string, InvalidToken>validateTokenOrError(token: stringtoken); const { const key: stringkey } = yield* this.
dependencies: {
    readonly Database: IDatabase;
    readonly Users: IUsers;
}
dependencies
.type Database: IDatabaseDatabase.
IDatabase.set(table: string, username: string, value: string, key?: string): Effect.fn.Return<{
    key: string;
}>
set
(
class TodosImplTodosImpl.TodosImpl.TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, task: stringtask ); return {id: stringid: const key: stringkey, task: stringtask}; } *TodosImpl.completeTask(token: string, id: string): EffectGen<void, NoSuchTask | InvalidToken>completeTask(token: stringtoken: string, id: stringid: string): type EffectGen<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>EffectGen<void, class NoSuchTaskNoSuchTask | class InvalidTokenInvalidToken> { const const username: stringusername = yield* this.TodosImpl.validateTokenOrError(token: string): EffectGen<string, InvalidToken>validateTokenOrError(token: stringtoken); const const exists: booleanexists = import Option
@since2.0.0@categorymodels@since2.0.0@categoryModels
Option
.const isSome: <string>(self: Option.Option<string>) => self is Option.Some<string>

Checks whether an Option contains a value (Some).

@example
import { Option } from "effect"

console.log(Option.isSome(Option.some(1)))
// Output: true

console.log(Option.isSome(Option.none()))
// Output: false
@see{@link isNone} for the opposite check.@categoryGuards@since2.0.0
isSome
(yield* this.
dependencies: {
    readonly Database: IDatabase;
    readonly Users: IUsers;
}
dependencies
.type Database: IDatabaseDatabase.IDatabase.get(table: string, username: string, key: string): Effect.fn.Return<Option.Option<string>>get(
class TodosImplTodosImpl.TodosImpl.TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, id: stringid )); if (!const exists: booleanexists) return yield* new constructor NoSuchTask<{}>(args: void): NoSuchTaskNoSuchTask(); yield* this.
dependencies: {
    readonly Database: IDatabase;
    readonly Users: IUsers;
}
dependencies
.type Database: IDatabaseDatabase.IDatabase.delete(table: string, username: string, key: string): Effect.fn.Return<void>delete(
class TodosImplTodosImpl.TodosImpl.TASKS_TABLE: "tasks"TASKS_TABLE, const username: stringusername, id: stringid ); } private *TodosImpl.validateTokenOrError(token: string): EffectGen<string, InvalidToken>validateTokenOrError(token: stringtoken: string): type EffectGen<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>EffectGen<string, class InvalidTokenInvalidToken> { const
const maybeUsername: Option.Option<{
    username: string;
}>
maybeUsername
= yield* this.
dependencies: {
    readonly Database: IDatabase;
    readonly Users: IUsers;
}
dependencies
.type Users: IUsersUsers.
IUsers.validateToken(token: string): Effect.fn.Return<Option.Option<{
    username: string;
}>>
validateToken
(token: stringtoken);
if (import Option
@since2.0.0@categorymodels@since2.0.0@categoryModels
Option
.
const isNone: <{
    username: string;
}>(self: Option.Option<{
    username: string;
}>) => self is Option.None<{
    username: string;
}>

Checks whether an Option represents the absence of a value (None).

@example
import { Option } from "effect"

console.log(Option.isNone(Option.some(1)))
// Output: false

console.log(Option.isNone(Option.none()))
// Output: true
@see{@link isSome} for the opposite check.@categoryGuards@since2.0.0
isNone
(
const maybeUsername: Option.Option<{
    username: string;
}>
maybeUsername
)) {
return yield* new constructor InvalidToken<{}>(args: void): InvalidTokenInvalidToken(); } if (token: stringtoken.String.length: number

Returns the length of a String object.

length
) {
return yield* new BadSignature();
Type 'YieldWrap<Effect<never, BadSignature, never>>' is not assignable to type 'YieldWrap<Effect<any, InvalidToken, never>>'. Type 'Effect<never, BadSignature, never>' is not assignable to type 'Effect<any, InvalidToken, never>'. Type 'BadSignature' is not assignable to type 'InvalidToken'. Types of property '_tag' are incompatible. Type '"BadSignature"' is not assignable to type '"InvalidToken"'.
} return
const maybeUsername: Option.Some<{
    username: string;
}>
maybeUsername
.
Some<{ username: string; }>.value: {
    username: string;
}
value
.username: stringusername;
} }

In Effect’s current canon, the entire layer definition gets marked as invalid, which isn’t helpful when trying to pinpoint issues.

context, dependencies, effunct

As mentioned before, the implementing utility returns an abstract superclass that provides a dependencies structure. The superclass also provides a context structure so you no longer have to repeat yourself when passing dependencies to Effects defined outside of the implementation scope. Revisiting our earlier example we now have

Before

const const helperFn: (someInput: number) => Effect.Effect<string, never, ServiceOne>helperFn = import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const fn: <YieldWrap<Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>> | YieldWrap<Effect.Effect<string, never, never>>, string, [someInput: number]>(body: (someInput: number) => Generator<YieldWrap<Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>> | YieldWrap<Effect.Effect<string, never, never>>, string, never>) => (someInput: number) => Effect.Effect<...> (+20 overloads)
fn
(function*(someInput: numbersomeInput: number) {
const
const serviceOne: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}
serviceOne
= yield* class ServiceOneServiceOne;
const const serviceOneResult: stringserviceOneResult = yield*
const serviceOne: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}
serviceOne
.function serviceOneMethod(someInput: number): Effect.Effect<string, never, never>serviceOneMethod(someInput: numbersomeInput);
// Do something complex with result before returning it. const const complexResult: stringcomplexResult = const serviceOneResult: stringserviceOneResult; return const complexResult: stringcomplexResult; }); const const ServiceTwoImpl: Layer.Layer<ServiceTwo, never, ServiceOne>ServiceTwoImpl = import LayerLayer.
const effect: <ServiceTwo, {
    serviceTwoMethod(someInput: number): Effect.Effect<string, never, never>;
}, never, ServiceOne>(tag: Context.Tag<ServiceTwo, {
    serviceTwoMethod(someInput: number): Effect.Effect<string, never, never>;
}>, effect: Effect.Effect<{
    serviceTwoMethod(someInput: number): Effect.Effect<string, never, never>;
}, never, ServiceOne>) => Layer.Layer<ServiceTwo, never, ServiceOne> (+1 overload)

Constructs a layer from the specified effect.

@since2.0.0@categoryconstructors
effect
(class ServiceTwoServiceTwo, import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.
const gen: <YieldWrap<Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>>, {
    serviceTwoMethod: (someInput: number) => Effect.Effect<string, never, never>;
}>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>>, {
    serviceTwoMethod: (someInput: number) => Effect.Effect<string, never, never>;
}, never>) => Effect.Effect<...> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to Use

Effect.gen allows you to write code that looks and behaves like synchronous code, but it can handle asynchronous tasks, errors, and complex control flow (like loops and conditions). It helps make asynchronous code more readable and easier to manage.

The generator functions work similarly to async/await but with more explicit control over the execution of effects. You can yield* values from effects and return the final result at the end.

Example

import { Effect } from "effect"

const addServiceCharge = (amount: number) => amount + 1

const applyDiscount = (
  total: number,
  discountRate: number
): Effect.Effect<number, Error> =>
  discountRate === 0
    ? Effect.fail(new Error("Discount rate cannot be zero"))
    : Effect.succeed(total - (total * discountRate) / 100)

const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))

const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))

export const program = Effect.gen(function* () {
  const transactionAmount = yield* fetchTransactionAmount
  const discountRate = yield* fetchDiscountRate
  const discountedAmount = yield* applyDiscount(
    transactionAmount,
    discountRate
  )
  const finalAmount = addServiceCharge(discountedAmount)
  return `Final amount to charge: ${finalAmount}`
})
@since2.0.0@categoryCreating Effects
gen
(function*() {
const
const serviceOne: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}
serviceOne
= yield* class ServiceOneServiceOne;
// Need to create context manually const const context: Context.Context<ServiceOne>context = pipe<Context.Context<never>, Context.Context<ServiceOne>>(a: Context.Context<never>, ab: (a: Context.Context<never>) => Context.Context<ServiceOne>): Context.Context<ServiceOne> (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

Details

The pipe function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together.

import { pipe } from "effect"

const result = pipe(input, func1, func2, ..., funcN)

In this syntax, input is the initial value, and func1, func2, ..., funcN are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned.

Here's an illustration of how pipe works:

┌───────┐    ┌───────┐    ┌───────┐    ┌───────┐    ┌───────┐    ┌────────┐
│ input │───►│ func1 │───►│ func2 │───►│  ...  │───►│ funcN │───►│ result │
└───────┘    └───────┘    └───────┘    └───────┘    └───────┘    └────────┘

It's important to note that functions passed to pipe must have a single argument because they are only called with a single argument.

When to Use

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g)

becomes:

import { pipe, Array } from "effect"

pipe(as, Array.map(f), Array.filter(g))

Example (Chaining Arithmetic Operations)

import { pipe } from "effect"

// Define simple arithmetic operations
const increment = (x: number) => x + 1
const double = (x: number) => x * 2
const subtractTen = (x: number) => x - 10

// Sequentially apply these operations using `pipe`
const result = pipe(5, increment, double, subtractTen)

console.log(result)
// Output: 2
@since2.0.0
pipe
(
import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const empty: () => Context.Context<never>

Returns an empty Context.

@example
import * as assert from "node:assert"
import { Context } from "effect"

assert.strictEqual(Context.isContext(Context.empty()), true)
@since2.0.0@categoryconstructors
empty
(),
import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.
const add: <ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>(tag: Context.Tag<ServiceOne, {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}>, service: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}) => <Services>(self: Context.Context<Services>) => Context.Context<ServiceOne | Services> (+1 overload)

Adds a service to a given Context.

@example
import * as assert from "node:assert"
import { Context, pipe } from "effect"

const Port = Context.GenericTag<{ PORT: number }>("Port")
const Timeout = Context.GenericTag<{ TIMEOUT: number }>("Timeout")

const someContext = Context.make(Port, { PORT: 8080 })

const Services = pipe(
  someContext,
  Context.add(Timeout, { TIMEOUT: 5000 })
)

assert.deepStrictEqual(Context.get(Services, Port), { PORT: 8080 })
assert.deepStrictEqual(Context.get(Services, Timeout), { TIMEOUT: 5000 })
@since2.0.0
add
(class ServiceOneServiceOne,
const serviceOne: {
    serviceOneMethod(someInput: number): Effect.Effect<string, never, never>;
}
serviceOne
)
); return { serviceTwoMethod: (someInput: number) => Effect.Effect<string, never, never>serviceTwoMethod: import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const fn: <YieldWrap<Effect.Effect<string, never, never>>, string, [someInput: number]>(body: (someInput: number) => Generator<YieldWrap<Effect.Effect<string, never, never>>, string, never>) => (someInput: number) => Effect.Effect<string, never, never> (+20 overloads)fn(function*(someInput: numbersomeInput) {
return yield* pipe<Effect.Effect<string, never, ServiceOne>, Effect.Effect<string, never, never>>(a: Effect.Effect<string, never, ServiceOne>, ab: (a: Effect.Effect<string, never, ServiceOne>) => Effect.Effect<string, never, never>): Effect.Effect<string, never, never> (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

Details

The pipe function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together.

import { pipe } from "effect"

const result = pipe(input, func1, func2, ..., funcN)

In this syntax, input is the initial value, and func1, func2, ..., funcN are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned.

Here's an illustration of how pipe works:

┌───────┐    ┌───────┐    ┌───────┐    ┌───────┐    ┌───────┐    ┌────────┐
│ input │───►│ func1 │───►│ func2 │───►│  ...  │───►│ funcN │───►│ result │
└───────┘    └───────┘    └───────┘    └───────┘    └───────┘    └────────┘

It's important to note that functions passed to pipe must have a single argument because they are only called with a single argument.

When to Use

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g)

becomes:

import { pipe, Array } from "effect"

pipe(as, Array.map(f), Array.filter(g))

Example (Chaining Arithmetic Operations)

import { pipe } from "effect"

// Define simple arithmetic operations
const increment = (x: number) => x + 1
const double = (x: number) => x * 2
const subtractTen = (x: number) => x - 10

// Sequentially apply these operations using `pipe`
const result = pipe(5, increment, double, subtractTen)

console.log(result)
// Output: 2
@since2.0.0
pipe
(
const helperFn: (someInput: number) => Effect.Effect<string, never, ServiceOne>helperFn(someInput: numbersomeInput), import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const provide: <ServiceOne>(context: Context.Context<ServiceOne>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, ServiceOne>> (+9 overloads)

Provides necessary dependencies to an effect, removing its environmental requirements.

Details

This function allows you to supply the required environment for an effect. The environment can be provided in the form of one or more Layers, a Context, a Runtime, or a ManagedRuntime. Once the environment is provided, the effect can run without requiring external dependencies.

You can compose layers to create a modular and reusable way of setting up the environment for effects. For example, layers can be used to configure databases, logging services, or any other required dependencies.

Example

import { Context, Effect, Layer } from "effect"

class Database extends Context.Tag("Database")<
  Database,
  { readonly query: (sql: string) => Effect.Effect<Array<unknown>> }
>() {}

const DatabaseLive = Layer.succeed(
  Database,
  {
    // Simulate a database query
    query: (sql: string) => Effect.log(`Executing query: ${sql}`).pipe(Effect.as([]))
  }
)

//      ┌─── Effect<unknown[], never, Database>
//      ▼
const program = Effect.gen(function*() {
  const database = yield* Database
  const result = yield* database.query("SELECT * FROM users")
  return result
})

//      ┌─── Effect<unknown[], never, never>
//      ▼
const runnable = Effect.provide(program, DatabaseLive)

Effect.runPromise(runnable).then(console.log)
// Output:
// timestamp=... level=INFO fiber=#0 message="Executing query: SELECT * FROM users"
// []
@see{@link provideService} for providing a service to an effect.@since2.0.0@categoryContext
provide
(const context: Context.Context<ServiceOne>context)
); }) } }));

After

import { const effunct: <EffectGeneratorFn extends (...args: any) => Effect.fn.Return<any, any, any>>(generatorFn: EffectGeneratorFn) => (...args: Parameters<EffectGeneratorFn>) => GeneratedEffect<ReturnType<EffectGeneratorFn>>effunct } from "effective-modules";

class class ServiceTwoImplServiceTwoImpl extends implementing<Context.Tag<Modules.ServiceTwo, IServiceTwo>>(module: Context.Tag<Modules.ServiceTwo, IServiceTwo>): ModuleSuperClass<Context.Tag<Modules.ServiceTwo, IServiceTwo>, None, None>implementing(
const modules: {
    ServiceOne: Context.Tag<Modules.ServiceOne, IServiceOne>;
    ServiceTwo: Context.Tag<Modules.ServiceTwo, IServiceTwo>;
}
modules
.type ServiceTwo: Context.Tag<Modules.ServiceTwo, IServiceTwo>ServiceTwo).uses: <Context.Tag<Modules.ServiceOne, IServiceOne>, []>(first: Context.Tag<Modules.ServiceOne, IServiceOne>) => ModuleSuperClass<Context.Tag<Modules.ServiceTwo, IServiceTwo>, Some<[Context.Tag<Modules.ServiceOne, IServiceOne>]>, None>uses(
const modules: {
    ServiceOne: Context.Tag<Modules.ServiceOne, IServiceOne>;
    ServiceTwo: Context.Tag<Modules.ServiceTwo, IServiceTwo>;
}
modules
.type ServiceOne: Context.Tag<Modules.ServiceOne, IServiceOne>ServiceOne) implements IServiceTwo {
*ServiceTwoImpl.serviceTwoMethod(someInput: number): EffectGen<string, never, never>serviceTwoMethod(someInput: numbersomeInput: number): type EffectGen<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>EffectGen<string, never, never> { return yield* pipe<Effect.Effect<string, never, Modules.ServiceOne>, Effect.Effect<string, never, never>>(a: Effect.Effect<string, never, Modules.ServiceOne>, ab: (a: Effect.Effect<string, never, Modules.ServiceOne>) => Effect.Effect<string, never, never>): Effect.Effect<string, never, never> (+19 overloads)

Pipes the value of an expression into a pipeline of functions.

Details

The pipe function is a utility that allows us to compose functions in a readable and sequential manner. It takes the output of one function and passes it as the input to the next function in the pipeline. This enables us to build complex transformations by chaining multiple functions together.

import { pipe } from "effect"

const result = pipe(input, func1, func2, ..., funcN)

In this syntax, input is the initial value, and func1, func2, ..., funcN are the functions to be applied in sequence. The result of each function becomes the input for the next function, and the final result is returned.

Here's an illustration of how pipe works:

┌───────┐    ┌───────┐    ┌───────┐    ┌───────┐    ┌───────┐    ┌────────┐
│ input │───►│ func1 │───►│ func2 │───►│  ...  │───►│ funcN │───►│ result │
└───────┘    └───────┘    └───────┘    └───────┘    └───────┘    └────────┘

It's important to note that functions passed to pipe must have a single argument because they are only called with a single argument.

When to Use

This is useful in combination with data-last functions as a simulation of methods:

as.map(f).filter(g)

becomes:

import { pipe, Array } from "effect"

pipe(as, Array.map(f), Array.filter(g))

Example (Chaining Arithmetic Operations)

import { pipe } from "effect"

// Define simple arithmetic operations
const increment = (x: number) => x + 1
const double = (x: number) => x * 2
const subtractTen = (x: number) => x - 10

// Sequentially apply these operations using `pipe`
const result = pipe(5, increment, double, subtractTen)

console.log(result)
// Output: 2
@since2.0.0
pipe
(
effunct<(someInput: number) => EffectGen<string, never, Modules.ServiceOne>>(generatorFn: (someInput: number) => EffectGen<string, never, Modules.ServiceOne>): (someInput: number) => Effect.Effect<string, never, Modules.ServiceOne>effunct(this.ServiceTwoImpl.helperFn(someInput: number): EffectGen<string, never, Modules.ServiceOne>helperFn)(someInput: numbersomeInput), import Effect
@since2.0.0@categorymodels@since2.0.0@categorymodels@since2.0.0@categorymodels
Effect
.const provide: <Modules.ServiceOne>(context: Context.Context<Modules.ServiceOne>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Modules.ServiceOne>> (+9 overloads)

Provides necessary dependencies to an effect, removing its environmental requirements.

Details

This function allows you to supply the required environment for an effect. The environment can be provided in the form of one or more Layers, a Context, a Runtime, or a ManagedRuntime. Once the environment is provided, the effect can run without requiring external dependencies.

You can compose layers to create a modular and reusable way of setting up the environment for effects. For example, layers can be used to configure databases, logging services, or any other required dependencies.

Example

import { Context, Effect, Layer } from "effect"

class Database extends Context.Tag("Database")<
  Database,
  { readonly query: (sql: string) => Effect.Effect<Array<unknown>> }
>() {}

const DatabaseLive = Layer.succeed(
  Database,
  {
    // Simulate a database query
    query: (sql: string) => Effect.log(`Executing query: ${sql}`).pipe(Effect.as([]))
  }
)

//      ┌─── Effect<unknown[], never, Database>
//      ▼
const program = Effect.gen(function*() {
  const database = yield* Database
  const result = yield* database.query("SELECT * FROM users")
  return result
})

//      ┌─── Effect<unknown[], never, never>
//      ▼
const runnable = Effect.provide(program, DatabaseLive)

Effect.runPromise(runnable).then(console.log)
// Output:
// timestamp=... level=INFO fiber=#0 message="Executing query: SELECT * FROM users"
// []
@see{@link provideService} for providing a service to an effect.@since2.0.0@categoryContext
provide
(this.context: Context.Context<Modules.ServiceOne>context)
); } private *ServiceTwoImpl.helperFn(someInput: number): EffectGen<string, never, Modules.ServiceOne>helperFn(someInput: numbersomeInput: number): type EffectGen<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>EffectGen<string, never, enum ModulesModules.function (enum member) Modules.ServiceOne = "ServiceOne"ServiceOne> { const const serviceOne: IServiceOneserviceOne = yield*
const modules: {
    ServiceOne: Context.Tag<Modules.ServiceOne, IServiceOne>;
    ServiceTwo: Context.Tag<Modules.ServiceTwo, IServiceTwo>;
}
modules
.type ServiceOne: Context.Tag<Modules.ServiceOne, IServiceOne>ServiceOne;
const const serviceOneResult: stringserviceOneResult = yield* const serviceOne: IServiceOneserviceOne.IServiceOne.serviceOneMethod(someInput: number): Effect.fn.Return<string, never, never>serviceOneMethod(someInput: numbersomeInput); // Do something complex with result before returning it. const const complexResult: stringcomplexResult = const serviceOneResult: stringserviceOneResult; return const complexResult: stringcomplexResult; } }

A little less repetition, keeping the code more focused.

Something to note is a friction point that Effective Modules introduces by yielding generators directly. While directly yielding an Effect Generator will properly return values, bubble up errors, and propagate requirements up the Effect call stack, the Generator itself is not an Effect, so you cannot directly pipe it to methods such as Effect.provide. Effective Modules offers the effunct utility to wrap generator functions such that they return an Effect when called. You could also just use Effect.fn, but effunct automatically passes the generator function’s name into Effect.fn, enabling tracing (Effect.fn(generatorFn.name)(generatorFn)). No need to worry about binding the method before passing it into effunct, the implementing superclass takes care of that by autobinding all instance methods.

Custom Initializer

To allow for complex initialization behavior, one can pass a generator function to the super call on class construction and (optionally) add a throws clause to the superclass to specify an error channel.

import { type 
type Initialize<Module extends { Layer: Layer<any, any, any>; new (): {} | { dependencies: any; }; }> = Module["Layer"] extends Layer.Layer<any, infer Error, infer Requirements> ? InstanceType<Module> extends {
    dependencies: any;
} ? Effect.fn.Return<InstanceType<Module>["dependencies"], Error, Requirements> : Effect.fn.Return<void, Error, Requirements> : never
Initialize
} from "effective-modules";
class class InitializationErrorInitializationError extends import DataData.
const TaggedError: <"InitializationError">(tag: "InitializationError") => new <A>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
    readonly _tag: "InitializationError";
} & Readonly<A>
@since2.0.0@categoryconstructors
TaggedError
("InitializationError")<{}> {}
class class MyModuleImplMyModuleImpl extends implementing<Context.Tag<Modules.MyModule, IMyModule>>(module: Context.Tag<Modules.MyModule, IMyModule>): ModuleSuperClass<Context.Tag<Modules.MyModule, IMyModule>, None, None>implementing(const MyModule: Context.Tag<Modules.MyModule, IMyModule>MyModule).uses: <Context.Tag<Modules.OtherModule, IOtherModule>, []>(first: Context.Tag<Modules.OtherModule, IOtherModule>) => ModuleSuperClass<Context.Tag<Modules.MyModule, IMyModule>, Some<[Context.Tag<Modules.OtherModule, IOtherModule>]>, None>uses(const OtherModule: Context.Tag<Modules.OtherModule, IOtherModule>OtherModule).throws: <InitializationError>() => ModuleSuperClass<Context.Tag<Modules.MyModule, IMyModule>, Some<[Context.Tag<Modules.OtherModule, IOtherModule>]>, Some<InitializationError>>throws<class InitializationErrorInitializationError>() implements IMyModule { constructor() { super(function*():
type Initialize<Module extends { Layer: Layer<any, any, any>; new (): {} | { dependencies: any; }; }> = Module["Layer"] extends Layer.Layer<any, infer Error, infer Requirements> ? InstanceType<Module> extends {
    dependencies: any;
} ? Effect.fn.Return<InstanceType<Module>["dependencies"], Error, Requirements> : Effect.fn.Return<void, Error, Requirements> : never
Initialize
<typeof class MyModuleImplMyModuleImpl> {
if (var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.Math.random(): number

Returns a pseudorandom number between 0 and 1.

random
() > .5) {
yield* new constructor InitializationError<{}>(args: void): InitializationErrorInitializationError(); } return { type OtherModule: IOtherModuleOtherModule: yield* const OtherModule: Context.Tag<Modules.OtherModule, IOtherModule>OtherModule } }) } } class MyModuleImplMyModuleImpl.
type Layer: Layer.Layer<Modules.MyModule, InitializationError, Modules.OtherModule>
Layer

When creating a custom initializer you need to yield and return the dependencies object manually. There’ll be a type error if any dependency specified in uses is missing from the returned object, or if the resulting error or requirements channel does not match what was passed to uses and throws.

import { type 
type Initialize<Module extends { Layer: Layer<any, any, any>; new (): {} | { dependencies: any; }; }> = Module["Layer"] extends Layer.Layer<any, infer Error, infer Requirements> ? InstanceType<Module> extends {
    dependencies: any;
} ? Effect.fn.Return<InstanceType<Module>["dependencies"], Error, Requirements> : Effect.fn.Return<void, Error, Requirements> : never
Initialize
} from "effective-modules";
class class InitializationErrorInitializationError extends import DataData.
const TaggedError: <"InitializationError">(tag: "InitializationError") => new <A>(args: VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => YieldableError & {
    readonly _tag: "InitializationError";
} & Readonly<A>
@since2.0.0@categoryconstructors
TaggedError
("InitializationError")<{}> {}
class class MyModuleImplMyModuleImpl extends implementing<Context.Tag<Modules.MyModule, IMyModule>>(module: Context.Tag<Modules.MyModule, IMyModule>): ModuleSuperClass<Context.Tag<Modules.MyModule, IMyModule>, None, None>implementing(const MyModule: Context.Tag<Modules.MyModule, IMyModule>MyModule).uses: <Context.Tag<Modules.OtherModule, IOtherModule>, []>(first: Context.Tag<Modules.OtherModule, IOtherModule>) => ModuleSuperClass<Context.Tag<Modules.MyModule, IMyModule>, Some<[Context.Tag<Modules.OtherModule, IOtherModule>]>, None>uses(const OtherModule: Context.Tag<Modules.OtherModule, IOtherModule>OtherModule).throws: <InitializationError>() => ModuleSuperClass<Context.Tag<Modules.MyModule, IMyModule>, Some<[Context.Tag<Modules.OtherModule, IOtherModule>]>, Some<InitializationError>>throws<class InitializationErrorInitializationError>() implements IMyModule { constructor() { super(function*():
type Initialize<Module extends { Layer: Layer<any, any, any>; new (): {} | { dependencies: any; }; }> = Module["Layer"] extends Layer.Layer<any, infer Error, infer Requirements> ? InstanceType<Module> extends {
    dependencies: any;
} ? Effect.fn.Return<InstanceType<Module>["dependencies"], Error, Requirements> : Effect.fn.Return<void, Error, Requirements> : never
Initialize
<typeof class MyModuleImplMyModuleImpl> {
yield* MyModule;
Type 'YieldWrap<Tag<Modules.MyModule, IMyModule>>' is not assignable to type 'YieldWrap<Effect<any, InitializationError, Modules.OtherModule>>'. Type 'Tag<Modules.MyModule, IMyModule>' is not assignable to type 'Effect<any, InitializationError, Modules.OtherModule>'. Types of property '[EffectTypeId]' are incompatible. Type 'VarianceStruct<IMyModule, never, Modules.MyModule>' is not assignable to type 'VarianceStruct<any, InitializationError, Modules.OtherModule>'. Type 'Modules.MyModule' is not assignable to type 'Modules.OtherModule'.
if (var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.Math.random(): number

Returns a pseudorandom number between 0 and 1.

random
() > .5) {
return yield* new constructor InitializationError<{}>(args: void): InitializationErrorInitializationError(); } if (var Math: Math

An intrinsic object that provides basic mathematics functionality and constants.

Math
.Math.random(): number

Returns a pseudorandom number between 0 and 1.

random
() > .1) {
return yield* new UnexpectedError();
Type 'YieldWrap<Effect<never, UnexpectedError, never>>' is not assignable to type 'YieldWrap<Effect<any, InitializationError, Modules.OtherModule>>'. Type 'Effect<never, UnexpectedError, never>' is not assignable to type 'Effect<any, InitializationError, Modules.OtherModule>'. Type 'UnexpectedError' is not assignable to type 'InitializationError'. Types of property '_tag' are incompatible. Type '"UnexpectedError"' is not assignable to type '"InitializationError"'.
} return { OtherModule: 2
Type 'number' is not assignable to type 'IOtherModule'.
} }) } }

Wishlist

That’s basically it for Effective Modules. It’s just a thin wrapper around Effect that makes your code feel a bit more sane.

Here’s a list of changes I wish would land in TypeScript and Effect so Effective Modules code could be even cleaner still.

key type in Effect Platform Services

The dependencies object works well for services which a developer manually defines because v3’s Context.Tag and v4’s Context.Service creates a ServiceClass instance which captures the key literal as a generic, while Effective modules uses the string enum member as both the Self and key types. But this dependencies construct falls apart (becomes untyped) for Effect Platform services because these extend from the base Tag/Service type which does not take in a generic key / Identifier and instead types this field to string. For those cases Effective Modules falls back to a runtime error if the key is missing from a custom initializer return, and a this.getDependency utility is provided to get dependency by tag.

class class NonMigratedModuleNonMigratedModule extends import Context
@since2.0.0@categorymodels@since2.0.0@categoryModels
Context
.const Tag: <"NonMigrated">(id: "NonMigrated") => <Self, Shape>() => Context.TagClass<Self, "NonMigrated", Shape>
@example
import * as assert from "node:assert"
import { Context, Layer } from "effect"

class MyTag extends Context.Tag("MyTag")<
 MyTag,
 { readonly myNum: number }
>() {
 static Live = Layer.succeed(this, { myNum: 108 })
}
@since2.0.0@categoryconstructors
Tag
("NonMigrated")<class NonMigratedModuleNonMigratedModule, INonMigratedModule>() {}
import { import FileSystemFileSystem } from "@effect/platform"; class class MyModuleImplMyModuleImpl extends implementing<Context.Tag<Modules.MyModule, IMyModule>>(module: Context.Tag<Modules.MyModule, IMyModule>): ModuleSuperClass<Context.Tag<Modules.MyModule, IMyModule>, None, None>implementing(const MyModule: Context.Tag<Modules.MyModule, IMyModule>MyModule).uses: <Context.Tag<FileSystem.FileSystem, FileSystem.FileSystem>, [typeof NonMigratedModule]>(first: Context.Tag<FileSystem.FileSystem, FileSystem.FileSystem>, others_0: typeof NonMigratedModule) => ModuleSuperClass<Context.Tag<Modules.MyModule, IMyModule>, Some<[Context.Tag<FileSystem.FileSystem, FileSystem.FileSystem>, typeof NonMigratedModule]>, None>uses(import FileSystemFileSystem.const FileSystem: Context.Tag<FileSystem.FileSystem, FileSystem.FileSystem>
@since1.0.0@categorymodel@since1.0.0@categorytag
FileSystem
, class NonMigratedModuleNonMigratedModule) implements IMyModule {
constructor() { super(function*():
type Initialize<Module extends { Layer: Layer<any, any, any>; new (): {} | { dependencies: any; }; }> = Module["Layer"] extends Layer<any, infer Error, infer Requirements> ? InstanceType<Module> extends {
    dependencies: any;
} ? Effect.fn.Return<InstanceType<Module>["dependencies"], Error, Requirements> : Effect.fn.Return<void, Error, Requirements> : never
Initialize
<typeof class MyModuleImplMyModuleImpl> {
/* No compile-time error because FileSystem["key"] is string, but this will cause a runtime error on Layer construction. In most cases I advise avoiding custom initializers so Effective Modules can initialize things for you. */ return { type NonMigrated: INonMigratedModuleNonMigrated: yield* class NonMigratedModuleNonMigratedModule, // The following needs to be added to prevent runtime error // "@effect/platform/FileSystem": yield* FileSystem.Filesystem } }) } *MyModuleImpl.someMethod(): EffectGen<void>someMethod(): type EffectGen<A, E = never, R = never> = Generator<YieldWrap<Effect.Effect<any, E, R>>, A, any>EffectGen<void> { this.
dependencies: {
    readonly NonMigrated: INonMigratedModule;
}
dependencies
;
const
const fs: FileSystem.FileSystem
fs
= this.getDependency: <Context.Tag<FileSystem.FileSystem, FileSystem.FileSystem>>(dependency: Context.Tag<FileSystem.FileSystem, FileSystem.FileSystem>) => FileSystem.FileSystemgetDependency(import FileSystemFileSystem.const FileSystem: Context.Tag<FileSystem.FileSystem, FileSystem.FileSystem>
@since1.0.0@categorymodel@since1.0.0@categorytag
FileSystem
);
// getDependency only works for modules specified in "uses" this.getDependency: <typeof NonMigratedModule | Context.Tag<FileSystem.FileSystem, FileSystem.FileSystem>>(dependency: typeof NonMigratedModule | Context.Tag<FileSystem.FileSystem, FileSystem.FileSystem>) => INonMigratedModule | FileSystem.FileSystemgetDependency(OtherModule);
Argument of type 'Tag<Modules.OtherModule, {}>' is not assignable to parameter of type 'typeof NonMigratedModule | Tag<FileSystem, FileSystem>'. Type 'Tag<Modules.OtherModule, {}>' is not assignable to type 'Tag<FileSystem, FileSystem>'. Types of property 'Service' are incompatible. Type '{}' is missing the following properties from type 'FileSystem': access, copy, copyFile, chmod, and 25 more.
} }

I propose that ServiceClass / TagClass become the only type for defining services. They canonically have a separate type passed in for the Self parameter and also track the literal for Key, whereas the platform services use the same type for both Self and Shape, making them susceptible to structural-typing-related bugs. That is, a service with a similar enough shape to FileSystem could accidentally substitute it. Ideally all service definitions have an identifying, nominal type and explicitly track a key literal.

If this change is too big an ask, perhaps we could at least have the Effect Platform services be subtypes of ServiceClass rather than the base Service so that they can get an explicit key and ideally a nominal Self, or perhaps move the explicit generic key / Identifier type up into the base Service type definition.

Nominal ADT enums in TypeScript

Effective Modules employed string enums for nominal Self types then needed a separate method to map members to an interface. Ideally though, TypeScript would offer a way to define an Abstract Data Type with nominally typed members.

Before

import { interfaces } from "effective-modules";

export enum Modules {
  Users = "Users",
  Todos = "Todos",
  Database = "Database"
}

export const modules = interfaces<Modules, {
  Users: IUsers;
  Todos: ITodos;
  Database: IDatabase;
}>(Modules);

After

import { interfaces } from "effective-modules";

export enum Modules {
  Users = IUsers,
  Todos = ITodos,
  Database = IDatabase
}

export const modules = interfaces(Modules);

Is this issue the right place to make some noise?

Generators AS Effects

What would be nice is if Effect Generators could be passed around as Effects. This would eliminate the need for syntax like Effect.fn, Effect.gen, effunct, Effect.fn.Return, etc. One could write and call pure generator functions, directly specifying the return type as an Effect.

Before

const helper: Effect<string, SomeError> = gen(function*() {
  if (someCondition)
    return yield* new SomeError();
  return "something";
});

const program: Effect<void> = gen(function*() {
  const value = yield* pipe(
    helper,
    handleSomeError
  );
  yield* log(value);
});

After

function* helper(): Effect<string, SomeError> {
  if (someCondition)
    return yield* new SomeError();
  return "something";
}

function* program(): Effect<void> {
  const value = yield* pipe(
    helper(),
    handleSomeError
  );
  yield* log(value);
}

Which looks cleaner to you? One of the biggest complaints about Effect is how ugly it looks. I think this is how we address that. The after example is basically no more noisy than async await code.

First-class effectful primitives in JavaScript

If we can achieve the above, we could someday get effectful primitives built into the language itself. This mirrors the async await journey: we started with Bluebird’s Promise.coroutine which took a generator function and returned a promise, then this eventually served as a basis for JavaScript’s async and await keywords proposals.

Before

function* helper(): Effect<string, SomeError> {
  if (someCondition)
    return yield* new SomeError();
  return "something";
}

function* program(): Effect<void> {
  const value = yield* pipe(
    helper(),
    handleSomeError
  );
  yield* log(value);
}

After

effectful function helper(): Effect<string, SomeError> {
  if (someCondition)
    resolve new SomeError();
  return "something";
}

effectful function program(): Effect<void> {
  const value = resolve pipe(
    helper(),
    handleSomeError
  );
  resolve log(value);
}

Here I’m imagining a future where keywords like effectful and resolve are part of the JavaScript language, similar to async and await but for effectful code.

Abstractify utility type in TypeScript

Currently TypeScript has no generic way to turn all members of an object type abstract.

interface ITodos {
  getTasks(token: string): EffectGen{task: string; id: string;}[], InvalidToken>
  createTask(token: string, task: string): EffectGen<{task: string; id: string;}, InvalidToken>
  completeTask(token: string, id: string): EffectGen<void, NoSuchTask | InvalidToken>;
}

// Ideally you'd have this
type AbstractITodos = Abstractify<ITodos>

// Which would be equivalent to this
type AbstractITodos = {
  abstract getTasks(token: string): EffectGen{task: string; id: string;}[], InvalidToken>
  abstract createTask(token: string, task: string): EffectGen<{task: string; id: string;}, InvalidToken>
  abstract completeTask(token: string, id: string): EffectGen<void, NoSuchTask | InvalidToken>;
}

The main benefit here would be that you’d no longer need the implements code when declaring a module class. Ideally the interface shape would just be inferred / enforced by the module passed to Effective Modules’ implementing utility.

Before

class TodosImpl extends 
const implementing: (mod: any) => {
    new (): {};
}
implementing
(
const Todos: {
    interface: ITodos;
}
Todos
) implements ITodos {
Class 'TodosImpl' incorrectly implements interface 'ITodos'. Property 'method' is missing in type 'TodosImpl' but required in type 'ITodos'.
}

After

class TodosImpl extends 
const implementing: (mod: any) => abstract new () => {
    abstract method(): string;
}
implementing
(
const Todos: {
    interface: ITodos;
}
Todos
) {
Non-abstract class 'TodosImpl' does not implement inherited abstract member method from class '{ method(): string; }'.
}

I haven’t figured out the most appropriate Github issue to go to with this use-case. Let me know if you find it.

Thoughts?

Drop a comment!