Typescript: Generic intersection is not sufficiently narrowed for assignment to conditional type of the same.

Created on 28 Jul 2019  路  8Comments  路  Source: microsoft/TypeScript


TypeScript Version: 3.6.0-dev.20190727


Search Terms:
generic is not assignable to conditional

Code

function test_1<T extends string>(param: T): void {
    // Type 'T' is not assignable to type 'T extends string ? T : never'.
    //   Type 'string' is not assignable to type 'T extends string ? T : never'.
    const a: T extends string ? T : never = param
}

Expected behavior:
T (which extends string) to be assignable to T extends string ? T : never.

Actual behavior:

Type 'string' is not assignable to type 'T extends string ? T : never'.

I have no idea why the type checker is trying to assign param as a string. The error is correct in what it states, which is that string is not assignable to T extends string ? T : never because string is not assignable to either T or never. However, param is a T, not a string and T _should_ be assignable to T extends string ? T : never unless that evaluates to never. Since the generic type definition for T is T extends string we can assert that the T extends string conditional statement must always be true. Therefore, T extends string ? T : never can never evaluate to false, thus it can never be never.

Playground Link: Playground

Related Issues:
Maybe the following, though I don't really grok root cause so I'm not sure:
https://github.com/microsoft/TypeScript/issues/30152
https://github.com/microsoft/TypeScript/issues/29939

Duplicate

Most helpful comment

You shouldn't think of any as being the widest possible type.

It's more of a turn-off-type-checking type

All 8 comments

This looks to be working as designed.

Since the generic type definition for T is T extends string we can assert that the T extends string conditional statement must always be true.

Constraints for type parameters are intentionally ignored when resolving conditional types. The fundamental reason for this is because assignability is not transitive and therefore using the constraint of a type parameter can introduce unsound simplifications. A trivial example:

type Example<T extends [any]> = T extends [number] ? true : false;

/**
 * The constraint of `T` in Example is always assignable to the extends
 * type, but we can create unassignable instantiations.
 */

type Example1 = Example<[boolean]>; // false

TypeScript choose false negatives (as in your case) over false positives.


I have no idea why the type checker is trying to assign param as a string.

Once non-simplification of the conditional type is assumed the error messages follows from the checker trying to relate a type parameter to some type by relating its constraint to the type.

I agree with your example that T extends [number] will sometimes be true, and sometimes be false. However, if you flip it around so you have

type Example<T extends [number]> = T extends [any] ? true : false

then I don't believe there is any possible generic instantiation of Example that would result in a false positive.

Perhaps I'm misunderstanding the issue with assignability not being transitive. Is there some design issue that makes it so the type checker can't narrow the generic type by what it extends when evaluating a conditional type?

TypeScript wont even try and simplify distributive conditional types like:

type Example<T extends [number]> = T extends [any] ? true : false

probably because in the general case it's very hard to get right. If T is never then the resulting conditional will be never, so eagerly simplifying to true would not be correct.

If you change the type to:

type Example2<T extends [number]> = [T] extends [any] ? true : false

then this _will_ simplify to true because the conditional is satisfied without needing the constraint.


Perhaps I'm misunderstanding the issue with assignability not being transitive. Is there some design issue that makes it so the type checker can't narrow the generic type by what it extends when evaluating a conditional type

Your example is a specific case, but the checker works in general cases. Roughly the form would be something like:

Given

1) T extends A
2) T extends B ? L : R

If the constraint of A is assignable to B then T is assignable to B, therefore reduce the conditional type because it will always be true. This is precisely transitivity:

T extends A, A extends B ===> T extends B.

Assignability is not transitive for various reasons therefore the checker does not use this reasoning. In your specific example A === B which I think is probably enough to rule out the degenerate cases, but it would require special logic in the checker for yet to be determined benefit.

For those who come after me, it appears the piece of the puzzle that I was missing is that { x: any } is assignable to { x: string }. As someone who avoids any like the plague, I was caught off guard that there are a few cases where you can assign from type A to type B where type B is _narrower_ than type A. This makes it so assignability is not transitive, counter to intuition. More details over in this comment https://github.com/microsoft/TypeScript/issues/31096#issuecomment-486616050.

I would love to see this addressed in some way. Reading over some other GitHub issues (now that I know what to search for) it sounds like there are some thoughts on this though I don't see an obvious place where discussion on the topic is happening. Those few narrowing assignment situations certainly are frustrating since they prevent this type of thing from working.

I'm guessing it is too much to hope for the compiler being "smart" about which generic constraints can safely be transitive in a conditional, and which ones cannot?

You shouldn't think of any as being the widest possible type.

It's more of a turn-off-type-checking type

FWIW I think this is a duplicate of #23132.

Thanks as usual for the excellent discussion getting to the bottom of this, everyone

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zhuravlikjb picture zhuravlikjb  路  3Comments

Roam-Cooper picture Roam-Cooper  路  3Comments

wmaurer picture wmaurer  路  3Comments

weswigham picture weswigham  路  3Comments

siddjain picture siddjain  路  3Comments