One issue arises when you omit the value
parameter; the compiler struggles to infer type T
and defaults to the constraint string or undefined. To alter this default behavior, set a default value for the generic parameter using the =
syntax:
const nullify = <T extends string | undefined = undefined>(
// default generic ----> ^^^^^^^^^^^
value?: T,
): T extends string ? string : null => {
if (value === undefined) return null as any;
return value as any;
};
The difference becomes noticeable only when value
is omitted:
const result1 = nullify('string'); // returns 'string' as expected
const result2 = nullify(undefined); // returns null as expected
const result3 = nullify(); // returns null as expected 👍
Great job!
The drawback is that technically, the caller can specify any value for T
they desire. So, an eccentric caller could do this:
const oops = nullify<string>(); // returns 'string', oops
// ---------------> ^^^^^^^^ <--- manual specification of string
Since string
is manually specified for T
, and the value
parameter is optional, there are no compiler errors. As a result, the compiler mistakenly considers a null
value as being of type string
.
Although this scenario is unlikely, you may choose to ignore it. TypeScript is actually unsound in several areas, so you wouldn't be worsening things significantly here.
However, if accuracy matters to you, consider refactoring. One approach is to align your generic type parameter with the tuple type of a rest parameter:
const nullify = <T extends [string] | [undefined?]>(
...[value]: T
): T extends [string] ? string : null => {
if (value === undefined) return null as any;
return value as any;
};
const result1 = nullify('string'); // returns 'string' as expected
const result2 = nullify(undefined); // returns null as expected
const result3 = nullify(); // returns null as expected
In this approach, passing an incorrect value becomes less straightforward since only cases where value
is missing or undefined
and T
expands [undefined?]
are acceptable:
const okayNow = nullify<[string]>(); // error thrown, as expected
Alternatively, instead of generics, you can utilize overloads to achieve similar results. For more information on setting default values for generic parameters, check out How to assign a default value to a generic parameter?.
Access the code in the Playground