Context.GenericTag

Effect’s dependency management can seem like magic. Before we dive into more advanced concepts, let’s peel back a few layers to see how it is implemented.

We can define a service class by extending Context.Tag (16) , or in a more explicit fashion using Context.GenericTag.

import { Context, Effect } from "effect";

interface RandomService {
	next: Effect.Effect<number>;
}

const RandomTag = Context.GenericTag<RandomService>("@myapp/RandomService");

const fakeImpl = {
	next: Effect.succeed(1),
};

const roll = Effect.gen(function* (_) {
	const randomImpl = yield* RandomTag;
	const randomValue = yield* randomImpl.next;
	return Math.ceil(randomValue * 6);
});

roll.pipe(
    Effect.provideService(RandomTag, fakeImpl),
    Effect.runSync,
);

In this case, Context.Tag (16) is probably easier. Context.GenericTag can be very useful when

  • you don’t want to define a whole service class (e.g. when the “service” is a primitive value)
  • you want to decouple service definition from Effect
import ThirdPartyService from "third-party";

// defining a class is overkill for a simple string
const LogLevel = Context.GenericTag<"INFO" | "DEBUG">("@myapp/LogLevel");

// this can be a quick way to get moving
const ThirdParty = Context.GenericTag<ThirdPartyService>("@myapp/ThirdParty");

From a tag we can construct a Context.Context. This is a type-safe wrapper on top of a JavaScript Map, where each key (a string) may have values of different types (the service implementation). Note that constructing a context is a synchronous (non-effectful) operation.

const RandomContext = RandomTag.context(fakeImpl);
console.log(RandomContext.unsafeMap);

Seeing this implementation explains why it is a good practice to use namespaced keys. The key is used as the key of a Map, so poorly named keys could lead to collisions. When using Effect.provideService (17) (and related functions) the services are added to an implicit “default” Context.

const A1 = Context.GenericTag<number>("A");
const A2 = Context.GenericTag<string>("A");

const useA = Effect.gen(function* (_) {
	const a1 = yield* A1;
	const a2 = yield* A2;
	console.log(`a1 = ${a1}, a2 = ${a2}`);
});

useA.pipe(
	Effect.provideService(A2, "not ideal"),
	Effect.provideService(A1, 42),
	Effect.runSync,
);

Context.GenericTag can take a second type parameter, which helps with type hinting when there are multiple distinct services of the same type. This would be particularly common for primitives.

const PrimaryUrl = Context.GenericTag<"primary", string>("@myapp/PrimaryUrl");
const ReadReplicaUrl = Context.GenericTag<"secondary", string>(
	"@myapp/ReadReplicaUrl",
);

const useDatabase = Effect.gen(function* (_) {
	const primary = yield* PrimaryUrl;
	const replica = yield* ReadReplicaUrl;
	// these values are distinct, because the _keys_ are distinct
	console.log(`primary = ${primary}, replica = ${replica}`);
});

// the type checker is able to tell we need two distinct services,
// even though both services are implemented as strings
useDatabase.pipe(
	Effect.provideService(
		PrimaryUrl,
		"postgres://user:pw@localhost:5432/primary",
	),
	Effect.provideService(
		ReadReplicaUrl,
		"postgres://user:pw@localhost:5432/secondary",
	),
	Effect.runSync,
);

When using the two parameter Context.GenericTag<Id, Service> format, make sure that the Id is globally unique. Otherwise the type-checker may get confused.

const StatusCode = Context.GenericTag<"status", number>("@myapp/http/Status");
const OrderStatus = Context.GenericTag<"status", string>(
	"@myapp/orders/Status",
);

const retrieveOrder = Effect.gen(function* (_) {
	const fetched = yield* StatusCode;
	if (fetched >= 400) {
		return yield* Effect.fail("oh dear");
	}
	const shipping = yield* OrderStatus;
	console.log(`your order is ${shipping}`);
});

// this passes type checking, even though we have not provided an OrderStatus
// (it will die at runtime)
retrieveOrder.pipe(
    Effect.provideService(StatusCode, 200),
    Effect.runSync,
);

// note that this is _only_ a type checker issue
// the runtime keys/values do not overlap
// the following code runs as expected
retrieveOrder.pipe(
	Effect.provideService(OrderStatus, "out for delivery"),
	Effect.provideService(StatusCode, 200),
	Effect.runSync,
);

One way to ensure that a type is globally unique is to use a unique symbol. This is a common convention in Effect code, especially older code that predates Context.Tag (16) .

interface Port {
	readonly _: unique symbol;
}

interface UserId {
	readonly _: unique symbol;
}

const Port = Context.GenericTag<Port, number>("@myapp/Port");
const UserId = Context.GenericTag<UserId, number>("@myapp/UserId");

const buildUserUrl = Effect.gen(function* (_) {
	console.log(`https://localhost:${yield* Port}/users/${yield* UserId}`);
});

buildUserUrl.pipe(
	Effect.provideService(Port, 5000),
	Effect.provideService(UserId, 7357),
	Effect.runSync,
);

Looking at this final form, we can see roughly how Context.Tag (16) works. There is a bit of magic going on behind the scenes, but it is not much more than a Context.GenericTag. Here is a side-by-side comparison of the two in action. As with errors, being able to define a typical service as a class enables some handy shortcuts.

import { Effect, Context } from "effect";

interface PlainRandom {
	next: Effect.Effect<number>;
}

const randomImpl = {
	next: Effect.sync(() => Math.random()),
};

// ---- using GenericTag for control
interface RandomServiceG {
	readonly _: unique symbol;
}

const RandomServiceG = Context.GenericTag<RandomServiceG, PlainRandom>(
	"@myapp/RandomServiceG",
);

//     ┌─── RandomServiceG
//     ▼
type RandomServiceGId = Context.Tag.Identifier<typeof RandomServiceG>;

//     ┌─── PlainRandom
//     ▼
type RandomServiceGService = Context.Tag.Service<typeof RandomServiceG>;

const Live = RandomServiceG.of(randomImpl);

const Test = RandomServiceG.of({
	next: Effect.succeed(0.5),
});

// ---- using Tag for convenience
export class RandomServiceT extends Context.Tag("@myapp/RandomServiceT")<
	RandomServiceT,
	PlainRandom
>() {
	static Live = this.of(randomImpl);

	static Test = this.of({
		next: Effect.succeed(0.5),
	});
}

//     ┌─── RandomServiceT
//     ▼
type RandomServiceTId = Context.Tag.Identifier<RandomServiceT>;

//     ┌─── PlainRandom
//     ▼
type RandomServiceTService = Context.Tag.Service<RandomServiceT>;

Let’s recap:

  • Context.GenericTag allows defining a tag representing a placeholder for a service
  • You can create a Context.Context using MyTag.context(impl)
  • a Context is a type-safe wrapper on top of a Map
  • when using tags, it is important to use unique key values and identifier types
  • Context.Tag (16) is (approximately) syntactic sugar on top of Context.GenericTag