Created
November 21, 2024 14:14
-
-
Save jakeisnt/887b00674bc2e09fc24e97f330d9418c to your computer and use it in GitHub Desktop.
`createEnv`: statically checked and typed environment variables with Zod
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { z } from 'zod'; | |
// System for creating dynamically accessible environment variables | |
// from a schema. | |
// Schema for the environment variables. | |
// `type` specifies the type of the environment variable. | |
// `fallback` is the fallback value if the environment variable is not set. | |
type EnvSchema<T extends string> = Record< | |
T, | |
{ | |
type: z.ZodType; | |
fallback: z.infer<z.ZodType>; | |
} | |
>; | |
// Type for the environment variables. | |
// We use zod metaprogramming to infer the type of the environment variables | |
// based on the type specified in the Zod schema. | |
type EnvType<T extends EnvSchema<string>> = { | |
[K in keyof T]: T[K]['type'] extends z.ZodType | |
? z.infer<T[K]['type']> | |
: never; | |
}; | |
/** | |
* Create an environment object from a schema. | |
* @param schema - The schema to create the environment object from. | |
* @returns The environment object. | |
* | |
* @example | |
* const env = createEnv({ | |
* PORT: { type: z.coerce.number().int().positive(), fallback: 5435 }, | |
* DUMPS_DIR: { type: z.string(), fallback: path.join(process.env.HOME || '', 'improvin/dumps') }, | |
* }); | |
* | |
* env.PORT; // 5435 | |
* env.DUMPS_DIR; // /Users/jake/Documents/improvin/dumps | |
*/ | |
const createEnv = <T extends EnvSchema<string>>(schema: T): EnvType<T> => { | |
const env = { | |
_cache: {} as EnvType<T>, | |
}; | |
for (const [key, config] of Object.entries(schema) as [ | |
keyof T, | |
T[keyof T], | |
][]) { | |
Object.defineProperty(env, key, { | |
get() { | |
if (this._cache[key]) { | |
return this._cache[key]; | |
} | |
const rawValue = | |
process.env[String(key)] || | |
(() => { | |
console.warn( | |
`${String(key)} not provided, defaulting to ${config.fallback}`, | |
); | |
return config.fallback.toString(); | |
})(); | |
const parsedValue = config.type.safeParse(rawValue); | |
if (!parsedValue.success) { | |
console.error( | |
`Invalid value for ${key.toString()}:`, | |
parsedValue.error, | |
); | |
return config.fallback; | |
} | |
this._cache[key] = parsedValue.data; | |
return this._cache[key]; | |
}, | |
}); | |
} | |
return env as EnvType<T>; | |
}; | |
export { createEnv }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment