Theme:
Tidbit
  • technical

Complex return types in TypeScript

Guy Waldman's ProfileGuy Waldman
July 17, 2024
Time to read:
3 minutes

##Use-case & problem

So I wanted to create a simple TypeScript function which returns an environment variable, and depending on whether it is required or not, it will:
  1. In the happy path, return the value as a string
  2. If the environment variable is not defined, either return a potentially undefined value (if not required) or throw an error (if required)
Sounds simple enough, right?
Well, the naive attempt will look something like this with some TypeScript magic:
1function getEnvVar<TReq extends boolean>( 2 envVarName: string, 3 required: TReq 4): TReq extends true ? string : string | undefined { 5 const value = process.env[envVarName]; 6 if (required && !value) { 7 throw new Error(`Missing environment variable: ${envVarName}`); 8 } 9 return value; 10} 11 12const required = getEnvVar("foo", true); // => string 13const optional = getEnvVar("bar", false); // => string | undefined 14
This does not work, however (try it out in the TypeScript playground.
You will get the following error:
ERROR
Type 'string | undefined' is not assignable to type 'TReq extends true ? string : string | undefined'.
    Type 'undefined' is not assignable to type 'TReq extends true ? string : string | undefined'.(2322)
Now, I honestly wish I could provide some solid type theory here and explain why the TypeScript type checker cannot infer that the that the returned value can be undefined, but I'm not sure why.
If you know, please reach out and let me know!

##The solution

TypeScript supports declaring overloads which allows you to define the signature for a function which can accept different types of parameters. JavaScript itself not supporting overloads per-se, but rather runtime type checking is used to check the argument types (or length in case of variadic parameters using the arguments keyword).
Note
This is a similar concept to monomorphization in other languages, where generic functions get compiled to a different implementation depending on the type. In vast constrast, however, in this case the implementation stays the same, but the returned type is different according to the generic type parameter.
Using overloading, this becomes simple - we simply define the signature for each type instantiation that the function can accept:
1function getEnvVar(envVarName: string, required?: true): string; 2function getEnvVar(envVarName: string, required?: false): string | undefined; 3 4function getEnvVar(envVarName: string, required?: boolean) { 5 const value = process.env[envVarName]; 6 if (required && !value) { 7 throw new Error(`Missing environment variable: ${envVarName}`); 8 } 9 return value; 10} 11 12const required = getEnvVar("foo", true); // => string 13const optional = getEnvVar("bar", false); // => string | undefined 14
As you can see, there are now two overloads, for each possible value of the required parameter.
The undefined case even works, which is useful for having the default case be that the environment variable is required.

Related content

    • technical|
    • productivity
    Git hooks for fun & profit
    Post
    August 9, 2022
    Using git hooks for developer workflow automation
    • technical|
    • web
    Web page to PDF
    Post
    April 3, 2021
    Trying to generate a PDF from a web page is not as straightforward as you would think
    • technical|
    • web|
    • react
    Button with a ripple effect
    Post
    October 9, 2019
    Creating a material design style button with a ripple effect - includes a React example
    • technical|
    • productivity
    Loading .env files with no dependencies
    Tidbit
    July 26, 2024
    Loading .env files easily in *NIX shells with no dependencies