By defining the environment variables as string literals, you can both validate their presence and the desired type at once:
TS Playground
function validateEnv<Keys extends readonly string[]>(
keys: Keys,
): string[] extends Keys ? Record<never, never> : Record<Keys[number], string> {
const result = {} as Record<Keys[number], string>;
for (const key of keys) {
const value = process.env[key];
if (!value) throw new Error(`Expected environment variable "${key}" not set`);
result[key as Keys[number]] = value;
}
return result;
}
const requiredEnvVars = ["SECRET_1", "SECRET_2"] as const;
const secrets = validateEnv(requiredEnvVars);
//^? const secrets: Record<"SECRET_1" | "SECRET_2", string>
// OR:
const {
SECRET_1,
//^? const SECRET_1: string
SECRET_2,
//^? const SECRET_2: string
SECRET_3, /* Error (as expected)
~~~~~~~~
Property 'SECRET_3' does not exist on type 'Record<"SECRET_1" | "SECRET_2", string>'.(2339) */
} = validateEnv(requiredEnvVars);
As you can see in the code above, the return type of the function is an object having only keys that correspond to the string literals and string
values at those keys. Attempting to destructure other keys will cause a compiler error.
The one minor annoyance of this technique is the necessity of using a const
assertion on the input array so that the compiler infers the string elements in the array as literals:
const requiredEnvVars = ["SECRET_1", "SECRET_2"] as const;
// ^^^^^^^^
Failing to do so will prevent the compiler from being able to infer the key literals, causing a return type that has no entries:
TS Playground
const secrets = validateEnv(["SECRET_1", "SECRET_2"]);
//^? const secrets: Record<never, never>
On the bright side: the release of TypeScript 5.0 is just around the corner, and the new const
type parameters feature will allow you to influence the inference behavior, making it unnecessary to use the const assertion on the argument value:
TS Playground (using nightly compiler)
// Same implementation as above
declare function validateEnv<const Keys extends readonly string[]>(
// ^^^^^
keys: Keys,
): string[] extends Keys ? Record<never, never> : Record<Keys[number], string>;
const secrets = validateEnv(["SECRET_1", "SECRET_2"]);
//^? const secrets: Record<"SECRET_1" | "SECRET_2", string>
// OR:
const {
SECRET_1,
//^? const SECRET_1: string
SECRET_2,
//^? const SECRET_2: string
SECRET_3, /* Error (as expected)
~~~~~~~~
Property 'SECRET_3' does not exist on type 'Record<"SECRET_1" | "SECRET_2", string>'.(2339) */
} = validateEnv(["SECRET_1", "SECRET_2"]);