In order to create a required application env variables file named env.d.ts
, I want to ensure that any modifications or additions to it will trigger TypeScript errors and runtime errors for the checkEnv
function if a value is not set.
To achieve this, I have created a top-level file called env.d.ts
to extend the process.env
:
declare global {
namespace NodeJS {
interface ProcessEnv {
PORT: string;
HOST: string;
}
}
}
export { }; // necessary for declarations to work
I then include this file in the tsconfig.json
:
{
"include": [
"./env.d.ts"
]
}
While looking for a workaround due to TypeScript's union to tuple type limitation, I discovered this solution. Is there an easier way to handle this?
// 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;
// Ensure `checkEnv` throws error if something new is added to `env.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) {
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);
}
}
}
checkEnv();