I want env.d.ts
file to represent required application env variables and make sure that any change or addition to it will result in typescript error as well as runtime error for checkEnv
function if value is not set.
To do that I create top level file env.d.ts
file to extend process.env
:
declare global {
namespace NodeJS {
interface ProcessEnv {
PORT: string;
HOST: string;
}
}
}
export { }; // required for declarations to work
And include this file in tsconfig.json
:
{
"include": [
"./env.d.ts"
]
}
Because of typescript union to tuple type I found this workaround to do it, but is there a simpler way?
// node process types
interface ProcessEnv {
[key: string]: string | undefined
TZ?: string;
}
declare var process: {
env: ProcessEnv
}
// app env types from `env.d.ts`
interface ProcessEnv {
PORT: string;
HOST: string;
// if something will be added to `env.d.ts` we need to ensure that `checkEnv` will show error
}
// https://github.com/sindresorhus/type-fest/blob/main/source/remove-index-signature.d.ts
type RemoveIndexSignature<ObjectType> = {
[KeyType in keyof ObjectType as {} extends Record<KeyType, unknown>
? never
: KeyType]: ObjectType[KeyType];
};
class UnreachableCaseError extends Error {
constructor(unrechableValue: never) {
super(`Unreachable case: ${unrechableValue}`);
}
}
function checkEnv() {
type AppEnv = Exclude<keyof RemoveIndexSignature<typeof process.env>, 'TZ'> // PORT | HOST
const envKeys: AppEnv[] = [
'HOST',
'PORT'
// 'X' error - nice
// no error if something will be added to `env.d.ts`
];
for (const envKey of envKeys) {
// use switch to ensure all keys from env union type are present in `envKeys` array
switch (envKey) {
case 'HOST':
case 'PORT': {
if (process.env[envKey] === undefined) {
throw new Error(`Env variable "${envKey}" not set`);
}
break;
}
default:
throw new UnreachableCaseError(envKey); // ts will show error if something will be added to `env.d.ts` - nice
}
}
}
checkEnv();