It's not an issue of stupidity, rather a matter of safety.
Consider the following example:
function fn3<T extends "a">(): T {
return "a" // error
}
const result = fn3<'a' & { tag: 2 }>().tag // 2
In this case, T
extends a
, but is not exactly 'a'.
Even though the result
appears to be 2
, in reality it evaluates to undefined
during runtime.
This discrepancy triggers the error from TypeScript. The generic parameter needs to align with the actual value at runtime, as demonstrated in your second example.
Now let's analyze your initial example:
function fn<T extends "a" | "b">(param: T): T {
if (param === "a") return "a"
else return "b"
}
The underlying issue:
'"a"' is compatible with type 'T', but 'T' could potentially be instantiated with another subtype within the constraint '"a" | "b"'
Keep in mind that T
does not have to strictly be equal to a
or b
. It can encompass any subset within this constraint/union.
For instance, you can utilize never
which signifies the bottom type within the type system:
const throwError = () => {
throw Error('Hello')
}
fn<'a' | 'b'>(throwError())
Is there any scenario where fn
will actually yield a
or b
? No, it will raise an error. While perhaps not the most optimal example, it serves to illustrate the concept of varying subtypes.
Lets test fn
with a different set of subtypes:
declare var a: 'a' & { tag: 'hello' }
const result = fn(a).tag // undefined
You might argue: You're bending the rules here. The type 'a' & { tag: 'hello' }
cannot be represented accurately during runtime. In truth, it can't. The tag
property will always resolve to undefined
in runtime.
However, within the confines of typing, such types are easily constructed.
SUMMARY
Avoid interpreting extends
as a strict equality operator. Instead, understand that T
may manifest as any subset within the prescribed constraint.
P.S. Remember, in TypeScript, types are immutable. Once you define type T
with a particular constraint, you cannot alter the generic parameter T
to enforce a different constraint. Hence, in your initial example, the return type of T
cannot solely be a
or b
; it will always be a | b
.