Optional generic parameters in TypeScript

Take this function from an earlier iteration of @yeldirium/result that takes a value and constructs a wrapper type around it:

const okay = function <TValue>(value: TValue): Okay<TValue> {
  return {
    isFailed: false,
    value
  }
}

What if you want the value to be undefined? You can call okay like so:

const result = okay(undefined);

That works, but it is ugly and contains superfluous code. Someone might use null instead of undefined and an inconsistent mess starts to spread.

The problem here is that optional parameters and parameters that may be undefined are not the same thing in TypeScript. See these two examples and the slight difference between them:

const okay = function <TValue>(value?: TValue): Okay<TValue> {

const okay = function <TValue>(value: TValue | undefined): Okay<TValue> {

Only the first version, the one with special syntax, allows calls without a parameter: okay(). In the second version the parameter is still required, it just may be undefined: okay(undefined).

In neither case TypeScript realizes that TValue is supposed to be undefined. If you try to return value inside your wrapper type as in the first code block above, you will always hit the problem that Okay<TValue | undefined> is not assignable to Okay<TValue>.

The solution to this is overloading:

const okay: {
  <TValue extends undefined>(): Okay<TValue>;
  <TValue>(value: TValue): Okay<TValue>;
} = function <TValue>(value?: TValue): Okay<TValue | undefined> {
  return {
    isFailed: false,
    value
  };
};

The signature on the actual function expression must include every possible way to use it. Thus function <TValue>(value?: TValue): Okay<TValue | undefined>. This makes the parameter optional and has the return type include undefined as a possibility. This signature on its own is definitely not what we want, since TValue and undefined are still disconnected types.

In the type annotation on okay there are two call signatures. The first of them omits the parameter entirely and is only active if TValue extends undefined, which is a fancy way of telling TypeScript that TValue is undefined. So if TValue is undefined, the function takes no parameter and returns Okay<TValue>, which translates to Okay<undefined>.

The second call signature includes the parameter and makes no assumptions about TValue. This is actually the same signature that okay had initially. This catches all other cases in which TValue is not undefined and thus a parameter is required.

References