Why can't the generic interface in TS infer the type correctly?
I would not say it is stupid. It just safe. C0nsider this example:
function fn3<T extends "a">(): T {
return "a" // error
}
const result = fn3<'a' & { tag: 2 }>().tag // 2
It means that T
extends a
but not equal 'a'.
In above example, result
is 2
but in runtime it equals undefined
.
This is why TS gives you an error. Generic parameter should be binded with runtime value. Just like you did in your second example.
Let's take a look on your first example:
function fn<T extends "a" | "b">(param: T): T {
if (param === "a") return "a"
else return "b"
}
The error:
'"a"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '"a" | "b"'
Please keep in mind, it does not mean that T
is always equal to a
or b
. T
can be any subtype of this constraint/union.
For instance, you can use never
which is the bottom type of type system:
const throwError = () => {
throw Error('Hello')
}
fn<'a' | 'b'>(throwError())
Is there any chance that fn
will return a
or b
? No, it will throw an error. Maybe this is not the best example just wanted to show you the meaning of different subtypes.
Let's call fn
with different set of subtypes:
declare var a: 'a' & { tag: 'hello' }
const result = fn(a).tag // undefined
You might say: Hey, you are not playing by the rules. Type 'a' & { tag: 'hello' }
is unrepresentable in runtime. In fact it is not. tag
will always be undefined
in runtime.
But, we are in a type scope. We can easily create such kind of type.
SUMMARY
Please dont treat extends
as an equal operator. It just means that T
might be any subtype of defined constraint.
P.S. Types are immutable in TypeScript. It means that once you created type T
with some constraint, you are unable to return same generic parameter T
with other constraint. I mean, in your first example, return type T
can't be only a
or only b
. It will always be a | b
This is currently a limitation in TypeScript's type checking system. This example can be better understood in the case of true | false
:
function returnSelf<T extends true | false>(param: T): T {
if (param === true) {
type CUR_VAL = T; // original `T`, not `true` if it narrowed
return true;
} else {
type CUR_VAL = T; // original `T`, not `false` if it narrowed
return false;
}
}
If you open the code in a playground and hover over the CUR_VAL
type alias, you will notice that it is still equal to T
, not the narrowed value. Therefore, when you try to return the value, it still thinks that T
is true | false
which makes it invalid.