Skip to content

Adding parentheses changes the result of type inference #56270

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

Closed
Max10240 opened this issue Oct 31, 2023 · 6 comments · Fixed by #56271
Closed

Adding parentheses changes the result of type inference #56270

Max10240 opened this issue Oct 31, 2023 · 6 comments · Fixed by #56271
Assignees

Comments

@Max10240
Copy link

Max10240 commented Oct 31, 2023

🔎 Search Terms

"constraint evaluation", "type infer"

🕗 Version & Regression Information

  • This changed between versions 4.9 and 5.0

In Version 5.x, parentheses changes the result of type inference;
In Version 4.9 - 4.7(The minimum version that supports infer X extends ...), parentheses not changes the result of type inference;

⏯ Playground Link

playground link

💻 Code

type PositiveInfinity = 1e999;
type NegativeInfinity = -1e999;

export type IsEqual<A, B> =
	(<G>() => G extends A ? 1 : 2) extends
	(<G>() => G extends B ? 1 : 2)
		? true
		: false;


export type Add<A extends number, B extends number> = [
	IsEqual<A, PositiveInfinity>, IsEqual<A, NegativeInfinity>,
	IsEqual<B, PositiveInfinity>, IsEqual<B, NegativeInfinity>,
] extends infer R extends [boolean, boolean, boolean, boolean]
	? [true, false] extends ([R[0], R[3]]) // Note: with parentheses
		? PositiveInfinity
		: 'failed'
	: never;

export type AddWithoutParentheses<A extends number, B extends number> = [
	IsEqual<A, PositiveInfinity>, IsEqual<A, NegativeInfinity>,
	IsEqual<B, PositiveInfinity>, IsEqual<B, NegativeInfinity>,
] extends infer R extends [boolean, boolean, boolean, boolean]
	? [true, false] extends [R[0], R[3]] // Note: without parentheses
		? PositiveInfinity
		: 'failed'
	: never;


type AddTest0 = Add<PositiveInfinity, PositiveInfinity>; // failed
type AddTest1 = AddWithoutParentheses<PositiveInfinity, PositiveInfinity>; // in version 5.x: "Infinity"; but in version 4.x, it's "failed"

🙁 Actual behavior

type AddTest0 = Add<PositiveInfinity, PositiveInfinity>; // "failed" (both in version 4.x and 5.x, it is)
type AddTest1 = AddWithoutParentheses<PositiveInfinity, PositiveInfinity>; // "Infinity" in version 5.x; but in version 4.x, it's "failed"

🙂 Expected behavior

// Better be able to do:
type AddTest0 = Add<PositiveInfinity, PositiveInfinity>; // Infinity (both in version 4.x and 5.x)
type AddTest1 = AddWithoutParentheses<PositiveInfinity, PositiveInfinity>; // Infinity (both in version 4.x and 5.x)


// at least:
type AddTest0 = Add<PositiveInfinity, PositiveInfinity>; // failed (both in version 4.x and 5.x)
type AddTest1 = AddWithoutParentheses<PositiveInfinity, PositiveInfinity>; // failed (both in version 4.x and 5.x)

Additional information about the issue

see also: #51090

@Andarist
Copy link
Contributor

The original PR that made the non-parenthesized version to work can be found here: #52091

@ahejlsberg
Copy link
Member

This is working as intended, though I agree it is a bit surprising. We gramatically check for conditional types of the pattern [X] extends [Y] ? ... (where [X] and [Y] are tuple types of the same arity) and defer their resolution if some of the X or Y elements are generic. This is such that non-distributable conditional types can be written [X] extends [Y] ? ... and be deferred similarly to X extends Y ? .... We document this behavior here.

We specifically don't first erase parentheses because that is just one of untold number of ways you could write a type that turns out to be a tuple type. The tuple types are in a sense immaterial (when they match on both sides they are effectively erased), but we want the explicit matching brackets on both sides to be the signal that this type is non-distributive.

@ahejlsberg ahejlsberg added the Working as Intended The behavior described is the intended behavior; this is not a bug label Oct 31, 2023
@jakebailey
Copy link
Member

Won't this mean that running a formatter which removes or adds parens will change behavior?

@Andarist
Copy link
Contributor

It does mean that - it happened to me just today when I was playing with this ;p

@jakebailey
Copy link
Member

I feel like unwrapping parens is a good idea exactly for that reason. Formatters frequently unparen or reparen expressions depending on their position and complexity. They won't add or remove other syntax, though, so I don't think we're breaking into the realm of the other ways to make a tuple type by handling it.

@Max10240
Copy link
Author

Max10240 commented Nov 1, 2023

defer their resolution if some of the X or Y elements are generic ...

export type AddWithInfer<A extends number, B extends number> = [
	IsEqual<A, PositiveInfinity>, IsEqual<A, NegativeInfinity>,
	IsEqual<B, PositiveInfinity>, IsEqual<B, NegativeInfinity>,
] extends infer R extends [boolean, boolean, boolean, boolean]
	? [true, false] extends (infer _ extends ([R[0], R[3]])) // Note: with infer
		? PositiveInfinity
		: 'failed'
	: never;

type AddTest2 = AddWithInfer<PositiveInfinity, PositiveInfinity>; // Infinity, works too!

@ahejlsberg So using "infer" makes it work, because "infer" leads to something like that(defer their resolution), right? 😃

@jakebailey jakebailey self-assigned this Nov 1, 2023
@jakebailey jakebailey removed the Working as Intended The behavior described is the intended behavior; this is not a bug label Nov 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants