The next few lessons will be quick. We have seen that Context is a type-safe wrapper around a Map. The API of the Context module matches this.
Context.add adds a service to a context. Like most things in Effect, this operation is immutable. The input context is not modified. Instead, the function returns a new Context object.
The Tag in question can be a Context.Tag (16)
or a Context.GenericTag (19)
. Once constructed, the two are essentially interchangeable.
import { Context } from "effect";
export class ActiveUser extends Context.Tag("@myapp/ActiveUser")< ActiveUser, { name: string }>() {}
const Greeting = Context.GenericTag<"GreetingId", string>("@myapp/Greeting");
const initial: Context.Context<"GreetingId"> = Greeting.context("Welcome");const combined: Context.Context<"GreetingId" | ActiveUser> = initial.pipe( Context.add(ActiveUser, { name: "Jane" }),);console.log(combined.unsafeMap); // Greeting and ActiveUserconsole.log(initial.unsafeMap); // unchanged: only GreetingNote that as we add more services to the context, the type of the context gets “bigger”. Consider how this corresponds to the Requirements type of an Effect. Context<R1 | R2> looks like an R1 | R2-shaped peg that fits into the R1 | R2-shaped hole of Effect<R1 | R2, E, A>.
The type of the service must match the type of the tag, otherwise TypeScript will produce a compile-time error.
initial.pipe( // tsserver says Argument of type 'number' is not assignable to parameter of type '{ name: string; }'. Context.add(ActiveUser, 123),);The order that distinct services are added to the context does not matter.
const combined2: Context.Context<"GreetingId" | ActiveUser> = ActiveUser.context({ name: "Quentin",}).pipe(Context.add(Greeting, "Bonjour"));console.log(combined2.unsafeMap); // essentially the same result as combinedBut if a service is added to the context that has the same key (e.g. "@myapp/Greeting") as an existing key, then the previous service will be overridden.
const combined3: Context.Context<"GreetingId"> = Greeting.context("Hello").pipe( Context.add(Greeting, "Bonjour"), Context.add(Greeting, "Guten tag"), Context.add(Greeting, "Hola"),);console.log(combined3.unsafeMap); // contains "Hola", not "Hello"This override behaviour will feature in many parts of Effect. It means a Context can behave as a scope for variables. Inside certain regions of our program we can override pieces of Context, and those values will reset outside of that region.
const outerContext = Greeting.context("Hello").pipe( Context.add(ActiveUser, { name: "Unknown user" }),);
const defaultSession = () => { console.log("doing regular stuff", outerContext.unsafeMap);};
const authenticatedSession = () => { const innerContext = outerContext.pipe( Context.add(ActiveUser, { name: "Steven" }), ); console.log("doing authenticated stuff", innerContext.unsafeMap);};
defaultSession();authenticatedSession();Note that operations on Context are synchronous. There are no Effects involved here. Context is a simple data type.
Let’s recap
Context.addadds a service to a Context- It returns a new, larger Context
- It does not mutate the Context passed in
- The service must be a valid implementation of what the tag requires
- Operations on Context are plain (non-effectful) operations