Issue
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();
Solution
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:
Answered By - halfdecent
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.