Context.add
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
or a Context.GenericTag
. 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 ActiveUser
console.log(initial.unsafeMap); // unchanged: only Greeting
Note 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 combined
But 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.add
adds 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