I am having a problem with typescript return types.
I have written the following function as a wrapper around process.env
:
function env<
RequiredType = undefined | boolean,
FallbackType = undefined | string,
>(
name: string,
required?: RequiredType,
fallback?: FallbackType,
): FallbackType extends string ? string : string | undefined {
//do some magic to return env variable
}
This function, as declared above, is a wrapper around process.env
. It takes a name as the first argument which is the name of an env var. The second and third argument are optional. When I set required to true, the functions return type should be infered as a string because if the env var is not defined and can not use a fallback the function runs process.exit(1)
. So in every scenario where required is set to true, it will return a string. The same is with fallback, if a fallback is set, the functions return type should be string because if an env var is not defined it will be replaced by a fallback value so it will retun a string anyways.
Setting the return type to string if an fallback value is given works just fine, but i can not get my head around an implementation for the required argument. An example would be:
const a = env("name") //infered type of a should be "string | undefined" (working)
const b = env("name", false) //infered type of "b" should be "string | undefined" (working)
const c = env("name", true) //infered type of "c" should be "string" (not working, should work because required is "true") <-------
const d = env("name", false, "This is my name") //infered type of "d" should be "string" (working because of fallback)
const e = env("name", true, "This is my name") //infered type of "e" should be "string" (working because of fallback, but should also work because required is "true")
CodePudding user response:
Recall that Typescript types don't exist at runtime. Thus, if you want to narrow the function signature for specific data passed, you have to teach Typescript about what subsets of data passed to your function will return in specific more narrowed return values. You can accomplish this with function overloading:
function env(name: string, required: true, fallback?: string): string;
function env(name: string, required: boolean|undefined, fallback: string): string;
function env(name: string, required?: boolean, fallback?: string): string | undefined;
function env(
name: string,
required?: boolean,
fallback?: string,
) {
const value = process.env[name] ?? fallback;
if(required && value === undefined) { process.exit(1); }
return value;
}
By providing multiple type signatures which are more narrow than the implementation signature, you can more directly control the return types Typescript is able to infer from a given usage of the function.
CodePudding user response:
For anyone interessted, here is the complete code:
import { logger } from "../Components/logger";
//defined overloads
export function env(name: string, required: true, fallback?: string): string;
export function env(
name: string,
required: boolean | undefined,
fallback: string,
): string;
export function env(
name: string,
required?: boolean,
fallback?: string,
): string | undefined;
//env wrapper function
export function env(name: string, required?: boolean, fallback?: string) {
//check if env is set
const isSet = process.env[name] !== undefined;
//check if env is not set and required
if (!isSet && required) {
//check if fallback is not set (everything that is a string and not set)
if (typeof fallback !== "string") {
//log error
logger.error(
`ENV-Var "${name}" is not set, required and has no fallback!`,
);
//exit process with error code 1
process.exit(1);
}
//log warning
logger.warn(
`ENV-Var "${name}" is not set, required but uses fallback value of "${fallback}"!`,
);
}
//return env when set else return fallback
return isSet ? process.env[name] : fallback;
}