Config
A Config<T> describes how to read and validate a single typed value of type
T from a ConfigProvider. Effect ships convenience constructors for the common
primitives, combinators for grouping and nesting them, and operators for
defaults, transformation, and fallback. Because every Config is also an
Effect, you consume it by yielding it inside Effect.gen.
import { Config, Effect } from "effect"
const program = Effect.gen(function* () { // Read and validate three values from the current ConfigProvider const host = yield* Config.string("HOST") // any string const port = yield* Config.port("PORT") // integer in 1–65535 const debug = yield* Config.boolean("DEBUG").pipe( // DEBUG is optional — fall back to false when it is missing Config.withDefault(false) )
yield* Effect.log(`http://${host}:${port} (debug=${debug})`)})
// HOST=localhost PORT=8080 DEBUG=yes node app.jsEffect.runFork(program)Each constructor takes the name of the key to read. With the default
environment-variable provider, Config.string("HOST") reads the HOST env var.
If a value is missing or fails validation, the program fails with a typed
Config.ConfigError rather than throwing.
Primitive constructors
Section titled “Primitive constructors”Effect provides constructors for the most common value types. Each one decodes the raw string from the provider and validates it:
| Constructor | Reads |
|---|---|
Config.string(name) | a string |
Config.nonEmptyString(name) | a string that must contain at least one character |
Config.number(name) | a number (allows NaN, Infinity) |
Config.finite(name) | a finite number (rejects NaN, Infinity) |
Config.int(name) | an integer (rejects floats) |
Config.port(name) | an integer in 1–65535 |
Config.boolean(name) | a boolean (true/false, yes/no, on/off, 1/0, y/n) |
Config.literal(value, name) | exactly the given literal value |
Config.literals(arr, name) | one of the given literal values |
Config.duration(name) | a Duration (e.g. "10 seconds") |
Config.url(name) | a URL |
Config.date(name) | a valid Date |
Config.logLevel(name) | a LogLevel ("Info", "Debug", …) |
Config.redacted(name) | a secret wrapped in Redacted — see Redacted Secrets |
import { Config, Effect } from "effect"
const program = Effect.gen(function* () { const timeout = yield* Config.duration("REQUEST_TIMEOUT") // "30 seconds" → Duration const level = yield* Config.logLevel("LOG_LEVEL") // "Debug" → LogLevel "Debug" const env = yield* Config.literals( ["development", "staging", "production"], "NODE_ENV" )
yield* Effect.log(`Running in ${env} with timeout ${timeout}`)})Providing defaults and making values optional
Section titled “Providing defaults and making values optional”Not every key is required. Config.withDefault supplies a fallback value, and
Config.option turns a missing value into Option.None.
import { Config, Effect, Option } from "effect"
const program = Effect.gen(function* () { // Use 8080 when PORT is absent const port = yield* Config.port("PORT").pipe(Config.withDefault(8080))
// Option<string>: Some when MAX_CONNECTIONS is set, None otherwise const maxConn = yield* Config.option(Config.int("MAX_CONNECTIONS"))
yield* Effect.log(`port=${port}, maxConn=${Option.getOrElse(maxConn, () => -1)}`)})Combining configs
Section titled “Combining configs”Config.all combines several configs into one. Pass a record to get a named
struct back, or an array to get a tuple:
import { Config, Effect } from "effect"
// Combine into a named struct — the parsed result mirrors this shapeconst ServerConfig = Config.all({ host: Config.string("HOST"), port: Config.port("PORT")})
const program = Effect.gen(function* () { const server = yield* ServerConfig // ^? { host: string; port: number } yield* Effect.log(`${server.host}:${server.port}`)})Transforming and validating
Section titled “Transforming and validating”Config.map post-processes a parsed value with a pure function. When the
transformation itself can fail, use Config.mapOrFail, which returns an
Effect and can produce a ConfigError.
import { Config } from "effect"
// A reusable Config for a custom domain typeclass HostPort { constructor( readonly host: string, readonly port: number ) {} get url() { return `${this.host}:${this.port}` }}
const hostPort = Config.all([Config.string("HOST"), Config.port("PORT")]).pipe( // Map the tuple into a domain object once both values are validated Config.map(([host, port]) => new HostPort(host, port)))For richer validation — multiple fields, refinements, branded types — reach for
a Schema rather than hand-rolling mapOrFail. See the next section.
Reading structured config with a Schema
Section titled “Reading structured config with a Schema”Config.schema builds a Config from any Schema.Codec. This is the most
powerful constructor — every primitive constructor above is just a shortcut for
it. Use it to read and validate an entire nested struct in one shot.
import { Config, Effect, Schema } from "effect"
// Describe the whole config tree with a Schemaconst AppConfig = Config.schema( Schema.Struct({ host: Schema.String, port: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })), features: Schema.Struct({ newDashboard: Schema.Boolean }) }), "app" // root path segment: keys live under the "app" namespace)
const program = Effect.gen(function* () { const config = yield* AppConfig // ^? { host: string; port: number; features: { newDashboard: boolean } } yield* Effect.log(`${config.host}:${config.port}`)})Validation failures from the schema (wrong type, out of range, missing key) are
wrapped in a Config.ConfigError whose cause is a SchemaError, so the error
message points at the exact path that failed.
Nesting configs under a namespace
Section titled “Nesting configs under a namespace”Config.nested scopes a config under a prefix. This keeps related keys grouped
and lets you reuse a config fragment at different paths.
import { Config, Effect } from "effect"
// Reusable fragment with un-prefixed keysconst connection = Config.all({ host: Config.string("HOST"), port: Config.port("PORT")})
const program = Effect.gen(function* () { // Reads DATABASE_HOST / DATABASE_PORT const db = yield* connection.pipe(Config.nested("DATABASE")) // Reads REDIS_HOST / REDIS_PORT const redis = yield* connection.pipe(Config.nested("REDIS"))
yield* Effect.log(`db=${db.host}:${db.port} redis=${redis.host}:${redis.port}`)})With the environment-variable provider, each nested segment becomes a
_-separated prefix on the env var name. With a JSON provider, it becomes an
extra object level. The mechanics of how a provider resolves these paths are
covered in Config Providers.
Handling config errors
Section titled “Handling config errors”A failing Config produces a Config.ConfigError. Its cause is either a
SourceError (the provider could not read the data) or a SchemaError
(validation failed). You can recover with the usual Effect error operators, or
fall back to another config with Config.orElse.
import { Config, Effect } from "effect"
const program = Effect.gen(function* () { // Try PRIMARY_URL first; if it errors for any reason, use the fallback const url = yield* Config.string("PRIMARY_URL").pipe( Config.orElse(() => Config.string("FALLBACK_URL")) ) yield* Effect.log(url)})Constructors reference
Section titled “Constructors reference”Every constructor below takes an optional name — the root path segment to read
from the provider. Omit name when the config is a fragment of a larger schema
(it will read at the current path). Each primitive is a thin shortcut over
Config.schema with a specific codec.
| Constructor | Codec | Result |
|---|---|---|
Config.string(name?) | Schema.String | string |
Config.nonEmptyString(name?) | Schema.NonEmptyString | string |
Config.number(name?) | Schema.Number | number |
Config.finite(name?) | Schema.Finite | number |
Config.int(name?) | Schema.Int | number |
Config.port(name?) | Config.Port | number |
Config.boolean(name?) | Config.Boolean | boolean |
Config.literal(value, name?) | Schema.Literal(value) | the literal |
Config.literals(arr, name?) | Schema.Literals(arr) | one of the set |
Config.duration(name?) | Schema.DurationFromString | Duration |
Config.url(name?) | Schema.URL | URL |
Config.date(name?) | Schema.DateValid | Date |
Config.logLevel(name?) | Config.LogLevel | LogLevel |
Config.redacted(name?) | Schema.Redacted(Schema.String) | Redacted<string> |
Config.schema(codec, path?) | any Schema.Codec | the codec’s type |
Config.succeed(value) | — | constant |
Config.fail(err) | — | always errors |
Config.make(parse) | — | custom |
Config.string(name?)
Section titled “Config.string(name?)”Reads a string using Schema.String. Any string value is accepted.
import { Config, ConfigProvider, Effect } from "effect"
const host = Config.string("HOST")
const provider = ConfigProvider.fromUnknown({ HOST: "localhost" })Effect.runSync(host.parse(provider))// => "localhost"Config.nonEmptyString(name?)
Section titled “Config.nonEmptyString(name?)”Reads a string but rejects the empty string (Schema.NonEmptyString).
import { Config, ConfigProvider, Effect } from "effect"
const name = Config.nonEmptyString("APP_NAME")
Effect.runSync(name.parse(ConfigProvider.fromUnknown({ APP_NAME: "api" })))// => "api"Effect.runSync(name.parse(ConfigProvider.fromUnknown({ APP_NAME: "" })))// => throws ConfigError (empty string rejected)Config.number(name?)
Section titled “Config.number(name?)”Reads a number using Schema.Number. Allows NaN and Infinity.
import { Config, ConfigProvider, Effect } from "effect"
const ratio = Config.number("RATIO")
Effect.runSync(ratio.parse(ConfigProvider.fromUnknown({ RATIO: "1.5" })))// => 1.5Config.finite(name?)
Section titled “Config.finite(name?)”Reads a finite number using Schema.Finite — rejects NaN and Infinity.
import { Config, ConfigProvider, Effect } from "effect"
const factor = Config.finite("FACTOR")
Effect.runSync(factor.parse(ConfigProvider.fromUnknown({ FACTOR: "2.0" })))// => 2Effect.runSync(factor.parse(ConfigProvider.fromUnknown({ FACTOR: "Infinity" })))// => throws ConfigError (not finite)Config.int(name?)
Section titled “Config.int(name?)”Reads an integer using Schema.Int — rejects floats.
import { Config, ConfigProvider, Effect } from "effect"
const retries = Config.int("MAX_RETRIES")
Effect.runSync(retries.parse(ConfigProvider.fromUnknown({ MAX_RETRIES: "3" })))// => 3Effect.runSync(retries.parse(ConfigProvider.fromUnknown({ MAX_RETRIES: "3.5" })))// => throws ConfigError (not an integer)Config.port(name?)
Section titled “Config.port(name?)”Reads an integer in 1–65535 (the Config.Port schema).
import { Config, ConfigProvider, Effect } from "effect"
const port = Config.port("PORT")
Effect.runSync(port.parse(ConfigProvider.fromUnknown({ PORT: "8080" })))// => 8080Effect.runSync(port.parse(ConfigProvider.fromUnknown({ PORT: "70000" })))// => throws ConfigError (out of range)Config.boolean(name?)
Section titled “Config.boolean(name?)”Reads a boolean using the Config.Boolean schema. Accepted
truthy strings: true, yes, on, 1, y; falsy: false, no, off, 0,
n.
import { Config, ConfigProvider, Effect } from "effect"
const debug = Config.boolean("DEBUG")
Effect.runSync(debug.parse(ConfigProvider.fromUnknown({ DEBUG: "yes" })))// => trueEffect.runSync(debug.parse(ConfigProvider.fromUnknown({ DEBUG: "0" })))// => falseConfig.literal(value, name?)
Section titled “Config.literal(value, name?)”Accepts exactly one literal value (Schema.Literal). The literal can be a
string, number, boolean, null, or bigint (AST.LiteralValue).
import { Config, ConfigProvider, Effect } from "effect"
const mode = Config.literal("production", "MODE")
Effect.runSync(mode.parse(ConfigProvider.fromUnknown({ MODE: "production" })))// => "production"Effect.runSync(mode.parse(ConfigProvider.fromUnknown({ MODE: "dev" })))// => throws ConfigErrorConfig.literals(arr, name?)
Section titled “Config.literals(arr, name?)”Accepts one of a fixed set of literal values (Schema.Literals).
import { Config, ConfigProvider, Effect } from "effect"
const env = Config.literals(["development", "production"], "NODE_ENV")
Effect.runSync(env.parse(ConfigProvider.fromUnknown({ NODE_ENV: "production" })))// => "production"Config.duration(name?)
Section titled “Config.duration(name?)”Reads a Duration from a human-readable string (Schema.DurationFromString),
e.g. "10 seconds", "500 millis", "Infinity".
import { Config, ConfigProvider, Effect } from "effect"
const timeout = Config.duration("TIMEOUT")
Effect.runSync(timeout.parse(ConfigProvider.fromUnknown({ TIMEOUT: "10 seconds" })))// => Duration { _tag: "Millis", millis: 10000 }Config.url(name?)
Section titled “Config.url(name?)”Reads a URL, parsing the string with the URL constructor (Schema.URL).
Fails if the string is not a valid URL.
import { Config, ConfigProvider, Effect } from "effect"
const apiUrl = Config.url("API_URL")
const url = Effect.runSync( apiUrl.parse(ConfigProvider.fromUnknown({ API_URL: "https://example.com" })))url.href// => "https://example.com/"Config.date(name?)
Section titled “Config.date(name?)”Reads a valid Date (Schema.DateValid). Fails with a SchemaError if the
string produces an invalid Date.
import { Config, ConfigProvider, Effect } from "effect"
const createdAt = Config.date("CREATED_AT")
Effect.runSync(createdAt.parse(ConfigProvider.fromUnknown({ CREATED_AT: "2024-01-15" })))// => Date("2024-01-15T00:00:00.000Z")Config.logLevel(name?)
Section titled “Config.logLevel(name?)”Reads an Effect LogLevel (the Config.LogLevel schema).
Accepted values: All, Fatal, Error, Warn, Info, Debug, Trace,
None.
import { Config, ConfigProvider, Effect } from "effect"
const level = Config.logLevel("LOG_LEVEL")
Effect.runSync(level.parse(ConfigProvider.fromUnknown({ LOG_LEVEL: "Info" })))// => "Info"Config.redacted(name?)
Section titled “Config.redacted(name?)”Reads a secret string wrapped in a Redacted container that hides the value
from logs and toString (Schema.Redacted(Schema.String)). See
Redacted Secrets for working with the
result.
import { Config, ConfigProvider, Effect } from "effect"
const apiKey = Config.redacted("API_KEY")
const secret = Effect.runSync( apiKey.parse(ConfigProvider.fromUnknown({ API_KEY: "sk-123" })))console.log(secret)// => <redacted>Config.schema(codec, path?)
Section titled “Config.schema(codec, path?)”The most general constructor: builds a Config<T> from any Schema.Codec.
Every primitive constructor above delegates to it. The optional path sets the
root segment(s) — pass a string for a flat key or an array for a nested path.
import { Config, ConfigProvider, Effect, Schema } from "effect"
// A struct read under the "db" namespaceconst DbConfig = Config.schema( Schema.Struct({ host: Schema.String, port: Schema.Int }), "db")
Effect.runSync( DbConfig.parse( ConfigProvider.fromUnknown({ db: { host: "localhost", port: 5432 } }) ))// => { host: "localhost", port: 5432 }
// A custom nested path with an arrayconst deep = Config.schema(Schema.String, ["a", "b", "c"])
Effect.runSync( deep.parse(ConfigProvider.fromUnknown({ a: { b: { c: "hi" } } })))// => "hi"Config.succeed(value)
Section titled “Config.succeed(value)”Creates a config that ignores the provider and always succeeds with value.
Useful as a constant fallback inside Config.orElse.
import { Config, ConfigProvider, Effect } from "effect"
const host = Config.string("HOST").pipe( Config.orElse(() => Config.succeed("localhost")))
Effect.runSync(host.parse(ConfigProvider.fromUnknown({})))// => "localhost"Config.fail(err)
Section titled “Config.fail(err)”Creates a config that always fails with a ConfigError wrapping the given
SourceError or SchemaError. Used inside orElse to re-raise a specific
error.
import { Config } from "effect"
// Inside an orElse handler, re-raise the original cause instead of recoveringconst reRaise = (cause: Parameters<typeof Config.fail>[0]) => Config.fail(cause)Config.make(parse)
Section titled “Config.make(parse)”Low-level constructor: builds a Config from a function that takes a
ConfigProvider and returns Effect<T, ConfigError>. Every primitive and
combinator is built on it; reach for it only when schema and the combinators
cannot express what you need.
import { Config, ConfigProvider, Effect } from "effect"
const hostPort = Config.make((provider) => Effect.all({ host: Config.string("host").parse(provider), port: Config.number("port").parse(provider) }))
Effect.runSync(hostPort.parse(ConfigProvider.fromUnknown({ host: "localhost", port: 3000 })))// => { host: "localhost", port: 3000 }Combinators reference
Section titled “Combinators reference”Config.map(self, f)
Section titled “Config.map(self, f)”Post-processes the parsed value with a pure function that cannot fail. Available data-first and data-last (pipeable).
import { Config, ConfigProvider, Effect } from "effect"
const upper = Config.string("NAME").pipe(Config.map((s) => s.toUpperCase()))
Effect.runSync(upper.parse(ConfigProvider.fromUnknown({ NAME: "alice" })))// => "ALICE"Config.mapOrFail(self, f)
Section titled “Config.mapOrFail(self, f)”Like map, but f returns Effect<B, ConfigError> so the transformation can
fail with a config error.
import { Config, ConfigProvider, Effect } from "effect"
const trimmed = Config.string("NAME").pipe( Config.mapOrFail((s) => Effect.succeed(s.trim())))
Effect.runSync(trimmed.parse(ConfigProvider.fromUnknown({ NAME: " bob " })))// => "bob"Config.orElse(self, that)
Section titled “Config.orElse(self, that)”Catches all ConfigErrors (including validation errors) and switches to the
fallback config. The that callback receives the error.
import { Config, ConfigProvider, Effect } from "effect"
const url = Config.string("PRIMARY_URL").pipe( Config.orElse(() => Config.string("FALLBACK_URL")))
Effect.runSync(url.parse(ConfigProvider.fromUnknown({ FALLBACK_URL: "https://b" })))// => "https://b"Config.withDefault(self, defaultValue)
Section titled “Config.withDefault(self, defaultValue)”Supplies a fallback value, but only when the failure is caused by missing
data (a missing key or undefined). Validation errors still propagate.
import { Config, ConfigProvider, Effect } from "effect"
const port = Config.number("PORT").pipe(Config.withDefault(3000))
Effect.runSync(port.parse(ConfigProvider.fromUnknown({})))// => 3000Effect.runSync(port.parse(ConfigProvider.fromUnknown({ PORT: "not-a-number" })))// => throws ConfigError (validation error is NOT defaulted)Config.option(self)
Section titled “Config.option(self)”Returns Some(value) on success and None when the data is missing. Like
withDefault, only missing-data errors produce None; validation errors still
propagate.
import { Config, ConfigProvider, Effect } from "effect"
const maybePort = Config.option(Config.number("PORT"))
Effect.runSync(maybePort.parse(ConfigProvider.fromUnknown({})))// => { _tag: "None" }Effect.runSync(maybePort.parse(ConfigProvider.fromUnknown({ PORT: "8080" })))// => { _tag: "Some", value: 8080 }Config.all(arg)
Section titled “Config.all(arg)”Combines multiple configs, mirroring the input shape: a record yields a struct, an array (or any iterable) yields a tuple/array.
import { Config, ConfigProvider, Effect } from "effect"
// Record → structconst asStruct = Config.all({ host: Config.string("host"), port: Config.number("port")})
Effect.runSync(asStruct.parse(ConfigProvider.fromUnknown({ host: "localhost", port: 5432 })))// => { host: "localhost", port: 5432 }
// Array → tupleconst asTuple = Config.all([Config.string("host"), Config.number("port")])
Effect.runSync(asTuple.parse(ConfigProvider.fromUnknown({ host: "localhost", port: 5432 })))// => ["localhost", 5432]Config.nested(self, name)
Section titled “Config.nested(self, name)”Prefixes every key the inner config reads with name. With fromEnv this is a
_-joined prefix; with fromUnknown/JSON it is an extra object level. Multiple
nested calls compose, outermost first.
import { Config, ConfigProvider, Effect } from "effect"
const host = Config.string("host").pipe(Config.nested("database"))
// JSON: extra object levelEffect.runSync(host.parse(ConfigProvider.fromUnknown({ database: { host: "localhost" } })))// => "localhost"
// Env: "_"-joined prefix (reads database_host)Effect.runSync(host.parse(ConfigProvider.fromEnv({ env: { database_host: "localhost" } })))// => "localhost"Schema helpers
Section titled “Schema helpers”These exported schemas back the primitive constructors. Use them directly with
Config.schema — for example to read a value at a
custom path, or to embed inside a larger Schema.Struct.
Config.Boolean
Section titled “Config.Boolean”Schema decoding a string into a boolean. Accepted strings: true, false,
yes, no, on, off, 1, 0, y, n.
import { Config, ConfigProvider, Effect } from "effect"
const flag = Config.schema(Config.Boolean, "ENABLED")
Effect.runSync(flag.parse(ConfigProvider.fromUnknown({ ENABLED: "on" })))// => trueConfig.Port
Section titled “Config.Port”Schema for an integer in 1–65535 (Schema.Int with a between check).
import { Config, ConfigProvider, Effect } from "effect"
const port = Config.schema(Config.Port, "PORT")
Effect.runSync(port.parse(ConfigProvider.fromUnknown({ PORT: "443" })))// => 443Config.LogLevel
Section titled “Config.LogLevel”Schema of LogLevel string literals: All, Fatal, Error, Warn, Info,
Debug, Trace, None.
import { Config, ConfigProvider, Effect } from "effect"
const level = Config.schema(Config.LogLevel, "LOG_LEVEL")
Effect.runSync(level.parse(ConfigProvider.fromUnknown({ LOG_LEVEL: "Debug" })))// => "Debug"Config.Record(key, value, options?)
Section titled “Config.Record(key, value, options?)”Builds a Schema (a union of a record and a flat-string parser) for a key-value
map. It accepts either a structured record from the provider or a flat string
like "k=v,k2=v2". Customize with separator (default ",") and
keyValueSeparator (default "="). Use it via Config.schema.
import { Config, ConfigProvider, Effect, Schema } from "effect"
const attributes = Config.schema( Config.Record(Schema.String, Schema.String), "OTEL_RESOURCE_ATTRIBUTES")
const provider = ConfigProvider.fromEnv({ env: { OTEL_RESOURCE_ATTRIBUTES: "service.name=my-service,service.version=1.0.0,custom.attribute=value" }})
Effect.runSync(attributes.parse(provider))// => {// "service.name": "my-service",// "service.version": "1.0.0",// "custom.attribute": "value"// }Config.TrueValues / Config.FalseValues
Section titled “Config.TrueValues / Config.FalseValues”The literal schemas backing Config.Boolean: Config.TrueValues is
["true", "yes", "on", "1", "y"] and Config.FalseValues is
["false", "no", "off", "0", "n"]. These are internal-ish but exported for reuse.
import { Config } from "effect"
Config.TrueValues.literals// => ["true", "yes", "on", "1", "y"]Config.FalseValues.literals// => ["false", "no", "off", "0", "n"]Wrap and unwrap
Section titled “Wrap and unwrap”Config.Wrap<A> recursively replaces the primitives in a structure with
Configs, so a caller can pass either a single Config<A> or a record of
individual Configs. Config.unwrap collapses such a value back into a single
Config<A>.
import { Config, ConfigProvider, Effect } from "effect"
interface Options { host: string port: number}
// Accept either Config<Options> or { host: Config<string>; port: Config<number> }const makeConfig = (config: Config.Wrap<Options>): Config.Config<Options> => Config.unwrap(config)
const config = makeConfig({ host: Config.string("host"), port: Config.number("port")})
Effect.runSync(config.parse(ConfigProvider.fromUnknown({ host: "localhost", port: 5432 })))// => { host: "localhost", port: 5432 }Config.Success<T> extracts the parsed value type from a Config:
import type { Config } from "effect"
type Host = Config.Success<Config.Config<string>>// => stringTypes and guards
Section titled “Types and guards”Config.isConfig(u)
Section titled “Config.isConfig(u)”Type guard returning true when u is a Config. Used internally by unwrap
and when narrowing an unknown value before calling .parse().
import { Config } from "effect"
Config.isConfig(Config.string("HOST"))// => trueConfig.isConfig("not a config")// => falseConfig.ConfigError
Section titled “Config.ConfigError”The error class produced by a failing config. Its cause is either a
SourceError (the provider could not read data) or a Schema.SchemaError
(validation failed). Match on error.cause._tag to distinguish the two.
import { Config, ConfigProvider, Effect, Schema } from "effect"
const program = Config.int("PORT").pipe( Effect.catch((error: Config.ConfigError) => Effect.succeed( Schema.isSchemaError(error.cause) ? "validation failed" : "could not read" ) ))
Effect.runSync(program.pipe( Effect.provideService(ConfigProvider.ConfigProvider, ConfigProvider.fromUnknown({ PORT: "x" }))))// => "validation failed"Config.Config<T>
Section titled “Config.Config<T>”The core interface. A Config<T> extends Effect<T, ConfigError>, so it can be
yielded directly in Effect.gen (resolving the current ConfigProvider from
context) or run against a specific provider with .parse(provider). It is also
pipeable, so config.pipe(Config.map(...)) works as shown throughout this page.