.env Files in Typescript Re-imagined
with the help of safe-ts-env
If you’ve ever had the pleasure of working on a project that depends on .env files for configuration, you’ve likely also had the displeasure of having values contained in the files be incorrect (hopefully just locally 😉). Let me show you how to solve this headache with just a handful of lines of code.
Setup
First, you'll need a TypeScript project to which you’ll add a couple of dependencies.
npm i zod safe-ts-envSecondly, you’ll need to convert your existing .env files into JSON. You can do this manually, or you can try out this prompt with your favorite LLM assistant.
I would like to convert all of the .env files in this repository to JSON files. You should maintain the key's 1:1 and try to convert the values to a reasonable type, defaulting to a string if unsure.Third, you’ll need to create a Zod object near where you plan on loading your configuration values. It should be based on the structure of the contents of your configuration files. Again, you can do this manually or try out this prompt.
I would like a zod object located @whereYouLoadConfig based on the contents of my configuration file.Usage
Believe it or not, the setup is 99% of the work. Because now all you have to do is…
import { getEnv } from 'safe-ts-env';
const checkedEnv = getEnv('path/to/env/file.json', myZodSchemaObject);Depending on what kind of project you’re in, you now have many ways of using these values.
A more “standard” approach might look like combining these with the process.env object
process.env = {
...process.env,
...Object.fromEntries(
Object.entries(checkedEnv).map(([key, value]) => [key, String(value)])
)
}Similarly, if you’re working on a CDK project, it might look something like
// Define your environment schema using Zod
const envSchema = z.object({
name: z.string(),
region: z.string(),
stage: z.enum(['dev', 'staging', 'prod']),
instanceCount: z.number().int().positive(),
});
const checkedEnv = getEnv('path/to/env/file.json', envSchema);
const stackProps = {
...process.env,
...Object.fromEntries(
Object.entries(checkedEnv).map(([key, value]) => [key, String(value)])
)
}
// Use in your CDK stack
new MyStack(app, 'MyStack', stackProps);The choice is left up to you on how you want to integrate it into your ecosystem.
“So what..?”
Cool, we’ve added a whole bunch of busy work for values that are configured correctly 90% of the time anyway. So what?
Just like I promised, we’ve now added a couple of major operational improvements with just a few lines of code.
When developers need to use an environment-dependent configuration value while writing code, the value will have static type assertions. Making the values much more straightforward to work with, as it removes the need for @ts-ignore’s and hard casting using as.
When the application uses the values, it will have runtime verification (gasp 😲). Now, not only are developers gaining an improved experience, but so are your users.
That’s just the tip of the iceberg. You’ll also now have
The ability to store nested values within your configuration.
Expand the strictness of both the static and runtime verification of the values via Zod
Share portions or entire schemas across projects (i.e., frontend, backend, and Ops). Leading to more people being on the same page with less work.
Conclusion
What more is there to say? Make an Ops person’s day and try out safe-ts-env



