Skip to content

Adding parentheses changes the result of type inference #56270

Description

@Max10240

🔎 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

Activity

  1. Andarist commented on Oct 31, 2023

    @Andarist
    Contributor

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

  2. ahejlsberg commented on Oct 31, 2023

    @ahejlsberg
    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.

  3. jakebailey commented on Oct 31, 2023

    @jakebailey
    Member

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

  4. Andarist commented on Oct 31, 2023

    @Andarist
    Contributor

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

  5. jakebailey commented on Oct 31, 2023

    @jakebailey
    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.

  6. Max10240 commented on Nov 1, 2023

    @Max10240
    Author

    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!

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

  7. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions