1
type Test = 'a' | 'b' | 'c';

function foo<T extends Test>(arg: T) {
  if (arg === 'a') {
    console.log(arg);
  }
}

like this.. I expect arg inferred 'a' in if block.

but ts infer as just T.

why this situation?

I think that extends keyword has something...

3
  • Interesting. If you write arg: string then it does narrow correctly, so it's definitely to do with T being a type variable. But if you don't have an extends clause then it thinks T and string have no overlap, so there is no way to test whether it's caused by extends specifically. It seems like a bug, but maybe there is a reason for it. Commented Feb 13, 2020 at 9:16
  • While the question itself might be interesting, the generics in above example is useless. Just use function foo(arg: Test) ... Commented Feb 13, 2020 at 9:16
  • @AlekseyL. You can presume that this is just a minimal reproducible example to demonstrate the issue, not the actual use-case. Commented Feb 13, 2020 at 10:55

1 Answer 1

1

The thing is that your type T is not Test but is a subset, we can say every type which can be used instead of T, and every for union means a type which has the same or less union members. Consider example type which extends Test:

type Test = 'a' | 'b' | 'c';
type SubTest = 'b' | 'c'

type IsSubTest = SubTest extends Test ? true : false // evaluates to true

As you can see SubTest doesn't have a member but it is assignable to Test, therefore we can use foo function with such a type.

const arg: SubTest = 'b'
foo(arg)

Everything good, no error. But that means that your condition arg === 'a' will never be met, as there is no member a in SubTest, that is why TS cannot assume that inside the condition we work with a, as in this situation this branch is totally not reachable and inside it will be type never. We can even write such function and check:

function foo(arg: SubTest) {
  if (arg === 'a') { // compilation error, condition will be always false
    console.log(arg);
  }
}

Ok but even though why TS is not narrowing to a, as the condition is clear anything which will pass it will be a, no doubt here!

Lets try reproduce manually the same type guard which exists in the original condition, and the condition checks if the left value say x (which extends from Test), equals to the right value say y(which extends Test)

function isTestMember<X extends Test, Y extends Test>(x: X, y: Y): x is Y { 
  return x == y  // error we cannot compare member of X with member of Y
}

As you can see such typeguard cannot be even written. As X and Y can not overlap, therefor the condition x === y is not valid for all members of X and Y

In summary TS cannot consider conditions on types which can not overlap as type guards, therefor type is not narrowed.

Sign up to request clarification or add additional context in comments.

16 Comments

If you define function isTestMember<Y extends Test>(x: Test, y: Y): x is Y { return x == y } then the foo function works fine, and arg is inferred as type T & 'a' inside the if block. So I don't think the fact that the condition isn't always satisfiable for some T really matters.
But x is not Test but T extends Test
So what? x is assignable to Test, since x: T and T extends Test. Just because T & 'a' might not be populated doesn't mean we can't narrow x to that type after an appropriate type guard; here it is, working without errors. Playground Link
In that case the return type of the isTestMember call is inferred as x is 'a', and it narrows x to type T & 'a', so the question is why doesn't arg === 'a' work the same way.
T & 'a' can evaluate to never, when we have such type like SubTest it will be exactly never. T & a is not any better then just T
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.