In a typical TypeScript project, there are two ways to handle configuration values.
Reading process.env Directly
In a Node.js environment, the most common way to access configuration is through the global process.env object:
const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY!;const port = process.env.PORT ? Number(process.env.PORT) : 3000;const logLevel = process.env.LOG_LEVEL ?? "info";
if (!process.env.DATABASE_URL) { throw new Error("DATABASE_URL is required");}
const databaseUrl = process.env.DATABASE_URL;There are two main issues with reading configuration directly from process.env.
First, because TypeScript types every environment variable as string | undefined, you have to handle the missing-value case before you can use the variable. To handle the missing-value case, you might use a ternary expression, an if statement, a nullish coalescing (??) operator, or sometimes the non-null assertion operator (!) to silence the type checker (as shown in the code snippet above).
But these fixes only address one variable at a time. They do not give you a consistent way to define which variables are required, which ones are optional, and what should happen when a value is missing.
Second, even when a variable exists, it still comes in as a string. That means you often need to convert it into the type your application needs, such as a number, boolean, URL, or array. That manual parsing and validation tends to spread across the application. Each variable needs its own conversion logic, its own validation rules, and often its own fallback behavior. Over time, configuration handling becomes harder to read, harder to reuse, and easier to get wrong.
In short, reading directly from process.env works for small cases, but it does not give you a clear, centralized, and type-safe way to define configuration.
Using Schema-Based Libraries
Because of the limitations of reading environment variables directly, you might decide to use a dedicated environment configuration library.
A good example is T3 Env. Instead of reading raw values from process.env throughout the application, you define a schema once, validate your environment variables up front, and then work with a typed configuration object in the rest of your code.
This is a major improvement over ad hoc process.env access. Configuration stops being an untyped bag of strings and becomes something explicit, validated, and reusable.
Where Effect Config Fits
Effect Config solves the same kinds of problems as schema-based configuration libraries, but for Effect applications.
In the next chapter, we will write our first configuration and see what this looks like in practice.