Coming From ZIO
If you are coming to Effect from ZIO, there are a few differences to be aware of.
Environment
In Effect, we represent the environment required to run an Effect
workflow as a union of services:
ts
import {Effect } from "@effect/io/Effect"// v---------v---- `R` is a union of Console | LoggertypeHttp =Effect <Console |Logger ,IOError |HttpError ,Response >typeResponse =Record <string, string>interfaceIOError {readonly_tag : "IOError"}interfaceHttpError {readonly_tag : "HttpError"}interfaceConsole {readonlylog : (msg : string) => void}interfaceLogger {readonlylog : (msg : string) => void}
ts
import {Effect } from "@effect/io/Effect"// v---------v---- `R` is a union of Console | LoggertypeHttp =Effect <Console |Logger ,IOError |HttpError ,Response >typeResponse =Record <string, string>interfaceIOError {readonly_tag : "IOError"}interfaceHttpError {readonly_tag : "HttpError"}interfaceConsole {readonlylog : (msg : string) => void}interfaceLogger {readonlylog : (msg : string) => void}
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]
Rationale
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
.
ts
interfaceA {readonlyprop : string}interfaceB {readonlyprop : number}constab :A &B = {Type 'string' is not assignable to type 'never'.2322Type 'string' is not assignable to type 'never'.: "", prop }
ts
interfaceA {readonlyprop : string}interfaceB {readonlyprop : number}constab :A &B = {Type 'string' is not assignable to type 'never'.2322Type 'string' is not assignable to type 'never'.: "", prop }
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 faciliate 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:
ts
interfaceA {readonlyprop : string}interfaceB {readonlyprop : number}constab :A |B = {prop : "",}
ts
interfaceA {readonlyprop : string}interfaceB {readonlyprop : number}constab :A |B = {prop : "",}
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
.