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.