Without the definition of bar this isn't a minimal reproducible example. Since all I know is that bar.getValue() returns a value of type string | number | boolean | undefined, I will make my own bar like this:
const bar = { getValue: (x: string): string | number | boolean | undefined => 12345 };
Then I write this code, which has no compiler errors:
const str = Math.random().toString();
getSomething("baz", str).then(v => console.log(v.toUpperCase()));
If you inspect the type of v with IntelliSense in your TypeScript IDE, it will be string. That's because the call signature of getSomething() says that it will return a promise of the same type as the second parameter. We passed in str, a string, so we get a Promise<string> out, right?
Oops, nope. Run the code and you'll get a runtime error and a message like TypeError: v.toUpperCase is not a function. Because at runtime, v will be 12345, the actual value returned from bar.getValue(). And 12345 is a number, which has no toUpperCase method. Somehow we got into a situation where a number was mistaken for a string. Where was the mistake?
It's exactly where the compiler warned you:
return foo === undefined ? defaultValue : foo; // error!
// Type 'string | number | boolean | T' is not assignable to type 'T'.
TypeScript is telling you that you are supposed to be returning a value of type T, but the compiler can only verify that you are returning a value of type string | number | boolean | T. In the case above, T was string, so you can interpret the error as something like "you're claiming to return a string but all I know is that you're returning a string | number | boolean, which might be a string but maybe it's a number or a boolean in which case your claim is incorrect and bad things might happen".
Hopefully you understand why this is a problem. T can be string or number or boolean, which are all narrower than the union type string | number | boolean. You can assign T to string | number | boolean but not vice versa.
About this question: "What could possibly be a subtype of string | number | boolean is what puzzled me. Aren't they already a primitive type?" Well, string is a subtype of string | number | boolean. A union A | B is a supertype of each of its members A and B.
Furthermore, even if you just had string or number or boolean, there are subtypes of these in TypeScript: there are string literal types like the type "foo", numeric literal types like the type 123, and even boolean literal types true and false (mentioned here and probably other places). These literal types represent specific values. So the types "a" and "b" are subtypes of string, and the types 1 and 2 are subtypes of number, and the types true and false are subtypes of boolean.
So in fact, when you call getSomething() with the second parameter as a string, numeric, or boolean literal, that's what gets inferred for T:
const p = getSomething("qux", "peanut butter and jelly");
// const p: Promise<"peanut butter and jelly">
So not only does p represent a promise of a string (which is not true in general), it actually represents a promise for the specific string "peanut butter and jelly". Oops.
So how do we fix this code? Well, that depends strongly on your use case. At first glance I'd say maybe it shouldn't be generic at all, and just allow both input and output to be string | number | boolean:
const getSomething2 = async (key: string, defaultValue: string | number | boolean):
Promise<string | number | boolean> => {
const foo = bar.getValue("foo");
return foo === undefined ? defaultValue : foo;
}
That compiles with no error, and then the earlier code that had a runtime error but no compiler error now gives you a nice compiler error:
getSomething2("baz", str).then(v => console.log(v.toUpperCase())); // error!
// ---------------------------------------------> ~~~~~~~~~~~
// Property 'toUpperCase' does not exist on type 'number'.
The value v is now known to be string | number | boolean, and you can't call a toUpperCase method on that because it might be a number or a boolean.
It's possible you need getSomething() to be generic, but in that case it would really matter what bar.getValue() does, and might require either bar.getValue()'s signature be modified, or a judicious type assertion somewhere inside getSomething() where you are assuming the responsibility for verifying something the compiler cannot, and dealing with the consequences if your assertion turns out to be untrue at runtime. Your answer with return foo === undefined ? defaultValue : foo as T; is unlikely to be the right sort of assertion, especially in light of literal types. I won't speculate further on this approach though. Suffice it to say that you'll need to think carefully about what claims you make when you use type assertions to make compiler errors go away.
Okay, hope that helps! Good luck!
Playground link to code
getSomething<string>('key', 'default'). Here function should returnstring, butstring|boolean|numberis returned.