The problem is that when you don't pass in a value argument, there is no value from which the compiler can infer T, and inference just fails. When inference fails, it generally falls back to its constraint, which in your case is string | undefined. If you want to change what it falls back to, you can set a generic parameter default using = 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;
};
And you'll only see a difference when you leave value out:
const result1 = nullify('string'); // string, as expected
const result2 = nullify(undefined); // null, as expected
const result3 = nullify(); // null, as expected
Hooray!
There is the drawback that technically the caller can specify any value of T that they want. So nothing stops a particularly unhinged caller from doing this:
const oops = nullify<string>(); // string, oops
// ---------------> ^^^^^^^^ <--- manually specify string
Since string is manually specified for T, and the value parameter is optional, there is no compiler error. And now you have a situation where the compiler mistakenly thinks a null value is of type string.
This is probably not very likely to happen, so you could just leave it alone and not worry about it. TypeScript is actually unsound in quite a few places so you wouldn't really be making things any worse here.
Still, if you care about such things, then you need to refactor. One way is to make your generic type parameter correspond to 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'); // string, as expected
const result2 = nullify(undefined); // null, as expected
const result3 = nullify(); // null, as expected
Now you cannot easily pass something incorrect, since the only acceptable situation in which value is missing or undefined is when T extends [undefined?]:
const okayNow = nullify<[string]>(); // error, as expected
The traditional way of doing this sort of thing is to use overloads instead of (or alongside) generics, but instead of going into this, I'll just point interested parties to How to give a default value to a generic parameter? for more information.
Playground link to code