-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
209: Practice Exercise: Building a Type-Safe Environment Variable Loader
Why can't I just use a type assertion like process.env as Env?
I see this all the time. It's tempting to just define an interface and tell TypeScript, "Trust me, these variables are there," using as Env. The problem is that type assertions are essentially you telling the compiler to stop complaining, but they don't actually change the runtime reality. If you forgot to add DATABASE_URL to your .env file, your app will boot up just fine, but it'll crash the moment it tries to connect to the database.
That's a nightmare to debug in production. Instead, we want "fail-fast" behavior. We want the app to scream and refuse to start if the environment is misconfigured. Here is the difference in approach:
// ❌ The "Trust Me" approach (Dangerous) interface Config { PORT: string; } const config = process.env as unknown as Config; // ✅ The "Verify" approach (Safe) function loadConfig(): Config { const port = process.env.PORT; if (!port) { throw new Error("Missing PORT environment variable"); } return { PORT: port }; }How do I handle variables that need to be numbers or booleans?
Since
process.envtreats everything as a string (or undefined), you're stuck doing a lot ofparseIntor=== 'true'checks. If you have ten different numeric variables, your loader becomes a mess of repetitive parsing logic.The cleanest way to handle this is to define a schema that describes not just the type, but how to transform the string. I usually prefer a simple mapping object for this. Look at how we can handle a
PORTnumber and aDEBUGboolean without writing tenifstatements:type EnvSchema = { PORT: (val: string) => number; DEBUG: (val: string) => boolean; API_KEY: (val: string) => string; }; const schema: EnvSchema = { PORT: (val) => parseInt(val, 10), DEBUG: (val) => val === 'true', API_KEY: (val) => val, }; function validateEnv<T>(schema: Record<keyof T, (val: string) => T[keyof T]>): T { const result = {} as any; for (const key in schema) { const value = process.env[key]; if (value === undefined) { throw new Error(`Missing env var: ${key}`); } result[key] = schema[key](value); } return result; }How do I make the typed config available throughout the app without re-validating?
You definitely don't want to run your validation logic every time you need a variable. That's a waste of resources and makes your code clunky. The trick is to validate once at the entry point of your application and export the resulting object as a constant.
I usually put this in a
config.tsfile. By exporting a constant, you get full autocomplete across your entire project, and you can be 100% certain that if the code is running, the variables are present and correctly typed.// config.ts const schema = { PORT: (val: string) => parseInt(val, 10), API_KEY: (val: string) => val, } as const; // We call the validator immediately export const ENV = validateEnv(schema); // In another file (e.g., server.ts) import { ENV } from './config'; // ENV.PORT is automatically recognized as a number! app.listen(ENV.PORT, () => console.log('Running...'));
📋 Practical Task
Exercise: Building a Type-Safe Config Loader for a Payment Gateway API
You are building an integration for a payment gateway. The application requires three environment variables to function: STRIPE_SECRET_KEY (string), MAX_RETRIES (number), and ENABLE_WEBHOOKS (boolean).
Your task is to implement a type-safe loader that:
- Defines a
PaymentConfiginterface with the correct types. - Implements a
loadPaymentConfigfunction that reads fromprocess.env. - Throws a descriptive error (e.g.,
"Missing environment variable: STRIPE_SECRET_KEY") if any of the variables are missing. - Correctly casts
MAX_RETRIESto a number andENABLE_WEBHOOKSto a boolean. - Exports a constant
CONFIGthat is the result of this validation.
Test your implementation by mocking process.env with some values and attempting to access CONFIG.MAX_RETRIES to ensure it is treated as a number by the TypeScript compiler.
There are no comments for now.