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();
CodePudding user response:
Maybe it would be simpler to use an enum
? The values of the enum
can be used both at compile-time for typing and at runtime.
enum EnvVariables {
PORT = 'PORT',
HOST = 'HOST'
}
We can use this to add the both PORT
and HOST
to the ProcessEnv
interface
.
type EnvVariablesType = {
[K in keyof typeof EnvVariables]: string
}
declare global {
namespace NodeJS {
interface ProcessEnv extends EnvVariablesType {}
}
}
Instead of having an array
with all environment variables, we can just use Object.values
to get all enum keys.
const envKeys = Object.values(EnvVariables).filter(value => typeof value === 'string') as (keyof typeof EnvVariables)[];
Also the switch
seems unnecessary.
for (const envKey of envKeys) {
if (process.env[envKey] === undefined){
throw new Error(`Env variable "${envKey}" not set`);
}
}
Let me know if this works for you.
Edit:
On second thought: Why not make it even simpler and use an array
?
// env.d.ts
const envVariables = ['HOST', 'PORT'] as const
type EnvVariablesType = {
[K in typeof envVariables[number]]: string
}
declare global {
namespace NodeJS {
interface ProcessEnv extends EnvVariablesType {}
}
}
// other file
function checkEnv() {
for (const envKey of envVariables) {
if (process.env[envKey] === undefined){
throw new Error(`Env variable "${envKey}" not set`);
}
}
}
checkEnv();
CodePudding user response:
Here's your types-only solution!
It does require an extra function to infer a few things for you, however, but I hope that isn't a big problem.
type HasAll<T extends ReadonlyArray<string>> = [T[number]] extends [AppEnv] ? [AppEnv] extends [T[number]] ? T : never : never;
Here we check if T
, an array includes exactly the members in the union AppEnv
with conditionals. The []
are there to prevent the conditional from being distributive and to prevent it from being a naked type.
We use this HasAll
validator in this helper function:
function hasAll<T extends ReadonlyArray<string>>(t: HasAll<T>): T { return t as T; }
TypeScript is smart enough to infer T
and pass it to HasAll
. If it passes the checks, then the type of the parameter t
is T
, otherwise it is never
.
This results in great type checking:
hasAll(["HOST"] as const); // error
hasAll(["HOST", "PORT"] as const); // fine
hasAll(["HOST", "PORT", "X"] as const); // error
However one limitation is that it does not error on duplicates:
hasAll(["HOST", "PORT", "HOST"] as const); // fine
Hopefully that isn't a big deal because while it is possible to check if there are duplicates, it's a lot of overhead and complexity for such a small thing.
You can see how this solution works with your use case below: