Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic intersection is not sufficiently narrowed for assignment to conditional type of the same. #32591

Closed
MicahZoltu opened this issue Jul 28, 2019 · 8 comments
Labels
Duplicate An existing issue was already created

Comments

@MicahZoltu
Copy link
Contributor

MicahZoltu commented Jul 28, 2019

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:
#30152
#29939

@jack-williams
Copy link
Collaborator

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.

@MicahZoltu
Copy link
Contributor Author

MicahZoltu commented Jul 28, 2019

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?

@jack-williams
Copy link
Collaborator

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.

@MicahZoltu
Copy link
Contributor Author

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 #31096 (comment).

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?

@AnyhowStep
Copy link
Contributor

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

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

@jack-williams
Copy link
Collaborator

FWIW I think this is a duplicate of #23132.

@RyanCavanaugh
Copy link
Member

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

@typescript-bot
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants