Skip to content

Coming From ZIO

If you are coming to Effect from ZIO, there are a few differences to be aware of.

In Effect, we represent the environment required to run an effect workflow as a union of services:

Example (Defining the Environment with a Union of Services)

1
import {
import Effect
Effect
} from "effect"
2
3
interface
interface IOError
IOError
{
4
readonly
(property) IOError._tag: "IOError"
_tag
: "IOError"
5
}
6
7
interface
interface HttpError
HttpError
{
8
readonly
(property) HttpError._tag: "HttpError"
_tag
: "HttpError"
9
}
10
11
interface
interface Console
Console
{
12
readonly
(property) Console.log: (msg: string) => void
log
: (
(parameter) msg: string
msg
: string) => void
13
}
14
15
interface
interface Logger
Logger
{
16
readonly
(property) Logger.log: (msg: string) => void
log
: (
(parameter) msg: string
msg
: string) => void
17
}
18
19
type
type Response = { [x: string]: string; }
Response
=
type Record<K extends keyof any, T> = { [P in K]: T; }

Construct a type with a set of properties K of type T

Record
<string, string>
20
21
// `R` is a union of `Console` and `Logger`
22
type
type Http = Effect.Effect<Response, IOError | HttpError, Console | Logger>
Http
=
import Effect
Effect
.
interface Effect<out A, out E = never, out R = never> namespace Effect

The `Effect` interface defines a value that lazily describes a workflow or job. The workflow requires some context `R`, and may fail with an error of type `E`, or succeed with a value of type `A`. `Effect` values model resourceful interaction with the outside world, including synchronous, asynchronous, concurrent, and parallel interaction. They use a fiber-based concurrency model, with built-in support for scheduling, fine-grained interruption, structured concurrency, and high scalability. To run an `Effect` value, you need a `Runtime`, which is a type that is capable of executing `Effect` values.

Effect
<
type Response = { [x: string]: string; }
Response
,
interface IOError
IOError
|
interface HttpError
HttpError
,
interface Console
Console
|
interface Logger
Logger
>

This may be confusing to folks coming from ZIO, where the environment is represented as an intersection of services:

type Http = ZIO[Console with Logger, IOError, Response]

The rationale for using a union to represent the environment required by an Effect workflow boils down to our desire to remove Has as a wrapper for services in the environment (similar to what was achieved in ZIO 2.0).

To be able to remove Has from Effect, we had to think a bit more structurally given that TypeScript is a structural type system. In TypeScript, if you have a type A & B where there is a structural conflict between A and B, the type A & B will reduce to never.

Example (Intersection Type Conflict)

1
interface
interface A
A
{
2
readonly
(property) A.prop: string
prop
: string
3
}
4
5
interface
interface B
B
{
6
readonly
(property) B.prop: number
prop
: number
7
}
8
9
const
const ab: A & B
ab
:
interface A
A
&
interface B
B
= {
10
// @ts-expect-error
11
(property) prop: never
prop
: ""
12
}
13
/*
14
Type 'string' is not assignable to type 'never'.ts(2322)
15
*/

In previous versions of Effect, intersections were used for representing an environment with multiple services. The problem with using intersections (i.e. A & B) is that there could be multiple services in the environment that have functions and properties named in the same way. To remedy this, we wrapped services in the Has type (similar to ZIO 1.0), so you would have Has<A> & Has<B> in your environment.

In ZIO 2.0, the contravariant R type parameter of the ZIO type (representing the environment) became fully phantom, thus allowing for removal of the Has type. This significantly improved the clarity of type signatures as well as removing another “stumbling block” for new users.

To facilitate removal of Has in Effect, we had to consider how types in the environment compose. By the rule of composition, contravariant parameters composed as an intersection (i.e. with &) are equivalent to covariant parameters composed together as a union (i.e. with |) for purposes of assignability. Based upon this fact, we decided to diverge from ZIO and make the R type parameter covariant given A | B does not reduce to never if A and B have conflicts.

From our example above:

1
interface
interface A
A
{
2
readonly
(property) A.prop: string
prop
: string
3
}
4
5
interface
interface B
B
{
6
readonly
(property) B.prop: number
prop
: number
7
}
8
9
// ok
10
const
const ab: A | B
ab
:
interface A
A
|
interface B
B
= {
11
(property) prop: string
prop
: ""
12
}

Representing R as a covariant type parameter containing the union of services required by an Effect workflow allowed us to remove the requirement for Has.

In Effect, there are no predefined type aliases such as UIO, URIO, RIO, Task, or IO like in ZIO.

The reason for this is that type aliases are lost as soon as you compose them, which renders them somewhat useless unless you maintain multiple signatures for every function. In Effect, we have chosen not to go down this path. Instead, we utilize the never type to indicate unused types.

It’s worth mentioning that the perception of type aliases being quicker to understand is often just an illusion. In Effect, the explicit notation Effect<A> clearly communicates that only type A is being used. On the other hand, when using a type alias like RIO<R, A>, questions arise about the type E. Is it unknown? never? Remembering such details becomes challenging.