Back to Blog
Lesson 45 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptSeptember 1, 20264 min read

Managing Environment Variables with Types for Safer Apps

Learn how to define process.env interfaces and validate configuration to prevent runtime errors. Keep your API client safe with typed environment variables.

TypeScriptConfigurationEnvironment VariablesAPI ClientSafety
Vibrant multicolored source code displayed on a computer screen, depicting programming and web development concepts.

Previously in this course, we finalized the architecture of our task client in final-refactoring-of-the-task-client-improving-api-architecture. Now, we’re moving from the code logic to the infrastructure that fuels it: how your application reads and validates its configuration.

In a production environment, you rarely hardcode API URLs or secrets. Instead, you rely on Environment Variables. However, relying on process.env directly is dangerous—it’s just a bag of strings that might be undefined, leading to "cannot read property of undefined" errors deep in your application logic. Today, we’ll fix that by enforcing type safety on your configuration.

From Raw Strings to Typed Config

TypeScript doesn't know what’s in your environment by default. Accessing process.env.API_URL returns a string | undefined, which forces you to write repetitive null checks everywhere. To build a robust system, we need to centralize this.

We treat our environment as a dependency. By creating a config.ts module, we define a "contract" for what our application requires to run. If an environment variable is missing, we fail fast at startup rather than crashing during a user request.

Defining Your Environment Interface

First, define the shape of your expected configuration. This ensures that every developer on your team knows exactly which variables are required.

TYPESCRIPT
// src/config.ts
export interface AppConfig {
  API_BASE_URL: string;
  API_TIMEOUT: number;
}

Now, we create a function that reads from process.env, validates the values, and returns the typed object.

TYPESCRIPT
// src/config.ts
export const getConfig = (): AppConfig => {
  const url = process.env.API_BASE_URL;
  const timeout = process.env.API_TIMEOUT;

  if (!url) {
    throw new Error("Missing required environment variable: API_BASE_URL");
  }

  return {
    API_BASE_URL: url,
    // Convert string to number, defaulting if necessary
    API_TIMEOUT: timeout ? parseInt(timeout, 10) : 5000,
  };
};

Integrating with the Task API Client

A detailed project timeline featuring design and development phases on a whiteboard with sticky notes.

In our running project, we previously set up a generic request wrapper. We should now update that wrapper to use our new getConfig helper, ensuring our API client is always configured correctly.

TYPESCRIPT
// src/api/client.ts
import { getConfig } from CE9178">'../config';

const { API_BASE_URL } = getConfig();

export async function fetchTask(id: string) {
  const response = await fetch(CE9178">`${API_BASE_URL}/tasks/${id}`);
  // ... rest of implementation
}

By doing this, you eliminate the risk of undefined leaking into your fetch calls. If API_BASE_URL is missing, the application throws an error the moment it tries to initialize the configuration, long before a user tries to load a task.

Practice Exercise

  1. Create a config.ts file in your project.
  2. Define an interface that includes API_KEY (string).
  3. Update your getConfig function to check for API_KEY.
  4. Refactor your TaskClient to use the API_KEY in the request headers.

Common Pitfalls

  • Trusting process.env directly: Always validate. Even if you think a variable is there, the deployment environment might be misconfigured.
  • Ignoring Type Conversion: Environment variables are always strings. If your app expects a number or boolean, you must parse them. Comparing "5000" === 5000 will fail in JavaScript.
  • Re-reading the config: Don't call getConfig() inside every function. Call it once at the top level or export a singleton object so you aren't parsing the environment on every network request.

For further reading on this, you might explore how typescript-mapped-types-for-environment-variables-a-pro-guide can automate some of this validation, or how to use typescript-environment-variables-preventing-runtime-config-errors to handle more complex scenarios.

FAQ

Yellow letter tiles spell 'questions' on a contrasting blue background.

Q: Should I put my secrets in process.env? A: Yes, but ensure you follow handling-sensitive-data-protecting-secrets-in-git-github to keep them out of your source control.

Q: Can I use Zod for this? A: Absolutely. While we used a manual check above, libraries like Zod are excellent for schema validation in larger projects.

Q: What if I have multiple environments (dev, prod)? A: That's typically handled by your build tool (like Vite or Webpack) or a package like dotenv. The typed getConfig approach remains the same regardless of how the variables get into process.env.

Recap

Team members presenting a project in a modern office setting with a focus on collaboration.

We’ve learned that environment variables are not just global strings; they are critical application configuration that requires strict typing. By defining an interface and a validation layer, we prevent runtime errors and ensure that our API client always has the data it needs to function correctly.

Up next: We will tackle complex data structures by learning how to handle deeply nested data and recursive interfaces.

Similar Posts