Optional Generic Function Argument
How to define a generic type argument for an optional function parameter in TypeScript
While performing a code review for Dinero.js, an awesome JavaScript money library maintained by Sarah Dayan, I noticed something odd. A newly added format function’s return type was unknown
, when it was expected to be a generic.
An optional transformer
parameter had recently been added to the function. If provided, the transformer should convert a money object to another type.
The Problem
function toDecimal<TOutput>(dineroObject: Dinero, transformer?: (value: string) => TOutput);
The intent was for TOutput
to be inferred by the transform function return type. The default argument to transformer
is a function that returns a string representation of a decimal.
There is no explicit return type. Type inference in the function definition was relied on to provide the return type.
function toDecimal(dineroObject, transformer = (value) => value as TOutput) { const value: string = toDecimalFn(dineroObject); return transformer(value);}
The return `value` has to be coerced to `TOutput`. Without using `as`, the compiler gave a very cryptic error.
Type 'string' is not assignable to type 'TOutput'.
'TOutput' could be instantiated with an arbitrary type which could be unrelated to 'string'.ts(2322)
After some research, I figured out what was happening. TOutput
is a generic argument and can be user provided. If provided, as in toDecimal<number>(d)
, there is a type conflict between the generic argument number
and the return type of default transformer
, which is string
. The generic type can not be inferred by a default argument.
Requirements to make the typing work as intended:
- If a transform argument isn’t provided:
- The
TOutput
generic argument should not be used or required. - The return type of
toDecimal
should bestring
.
- The
- When a transform argument is provided:
TOutput
should be inferred from the return type of thetransformer
argument.- The return type of
toDecimal
should beTOutput
I spend a lot of time thinking though how to use conditional types to solve this issue. Though fortunately Sarah put out a call on Twitter for help and Matt Pocock kindly provided an answer, use overloads.
The Fix: Using Overloads
Overloading the function declaration of toDecimal
allows TOutput
to be an optional generic argument. If not provided, the return type is string
. Though if provided, the return type of toDecimal
is inferred by the return type of the transformer
, just as intended!
Since TOutput
can’t be inferred by the return type of a default transformer
argument, it was removed in favor of branching. In the overload declaration, TOutput
doesn’t even exist if a transformer
isn’t provided. Coercing it into TOutput
was wrong, because it that situation it should explicitly be a string
not a generic.
function toDecimal(dineroObject: Dinero): string;function toDecimal<TOutput>(dineroObject: Dinero, transformer: (value: string) => TOutput): TOutput;function toDecimal<TOutput>(dineroObject: Dinero, transformer?: (value: string) => TOutput) { const value: string = toDecimalFn(dineroObject); if (!transformer) return value; return transformer(value);}
Link to the actual source of toDecimal with overloads
I hope this little case study helped you understand how to implement an optional generic argument in your own project.