Skip to content

Declaration merging can break equality tests for named tuples #61162

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
LukeAbby opened this issue Feb 11, 2025 · 4 comments
Closed

Declaration merging can break equality tests for named tuples #61162

LukeAbby opened this issue Feb 11, 2025 · 4 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@LukeAbby
Copy link

🔎 Search Terms

named tuples, equal, unequal

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ

This happens in typescript@next, typescript@latest, and all the way back to [email protected] (the earliest version that has tuples in the playground).

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/PTAEBUAsEsGdQKYEcCuBDANtALgT1AMaQIEDWochA9gLY1UB2G+KsCAJhQ6FgEYBOaftASwAdKACS2AOTxWHULxJoFFWfDShY2YQWwJ+2hAzagqAM0SpMOfNkhpuNFNnQZQaWLGgBzBmi80Fh4YgBQIFKgAA6GsIyYzKCC3NAM2FTqoA6UaZ7JCNGZrGm+oAAGAG44otjl4XixoACiNhiwADzgADSgAKoAfKAAvGGgoAAUHQBqAxMAlCND04gAHgYM7PDgoAD82fwoCKAAXKAWmGyLCOsmW5Mzc4vDy2sb9317B0en55cI8zG4y+uiOQPGZwu7QQAG4wmFGsdwLU+gwAjQOOAUNEMKIRi02p0ANo6YQMXwAXV6JN0pQpAzhkXGAD1vggImAoLl4PwEFhAklQQhwpEufBKFhSHz8MoCKozFo3DiEHJQOjjpQnBVJOwTNhoBYRPxyp5NvlokJsOYrA5jnKiMcpbgRWAAEIqNS2hW80AoNFoDGcW34aL8KixfhJXwIK0OKgoXyQK2WTzi3XpaByjASCZQJykeAAQU2QjgVosVCMOXghs2AEJAQjcE0APLRfUJDBY5X4okWwQ0XZnUl0uFNprInTgZsIQv82BtjsBLvY3HwYYE9ydRfQTvd3G9Hd71cIBkc4GsoXnsUUHl86AC+yHWHZYjZGcUe5CY6YADuaFweBkHccJxyRWp9zxDdWi3Do+yEANh1pckqVAeCByQslKTPJlQFZKE2HPABlSB4wwThlAqIV6nhREIFqI9l0g9dN0uOD+wDIdtGQylqQ4wdMLpHCwBZP5oWI0iUHIpRjnKajQJuIp+CtABvABfMddQIDBv1AXwMCoXhMFAFTwS4Ax+AuAhjkLfhBFwLohlM4EXNAUU33VeB2EyBgqCtGg0GwCzwlc4FIniDEAFkY1I9gOhbOY-OIfgzmadZBH0eLenAAZ5jOIk0t0NBMp6UAEopGE3LAIj-2iaJSnMVxzkrV9KACqV4EYJJwyXYylTXagaH7Y4-WAxJnTMlyIoQaK4zihKJiSwwzhbN47m2L5VrOJxcDytCdhud54FW-Ydt+HaKqqiAYHgdq8V4JLzHbXdl1NThfIYABaHqXr6k94AIWhht9BgxowZgQuBNSwjUoA

💻 Code

// This equality check is commonly used in libraries. It's used because it's a stricter sense of equality than mutual assignability.
// I personally ran into it this in a repo using `vitest`.
type Equals<T, U> =
  (<V>() => V extends T ? true : false) extends (<V>() => V extends U ? true : false)
    ? true
    : false;

type TestUnnamedTuples = Equals<[string], [string]>;
//   ^ true
// This is reliably true.
// This is likely because a tuple's name is an `Identifier` and a part of the cache key.
// Because these are unnamed they properly get thought of as identical. (Thanks Andarist for this find!)

type OptionalTuple = [param?: string];

type TestTypeAliasOptionalTuples = Equals<OptionalTuple, OptionalTuple>;
//   ^ true
// This is reliably true; the type ids are always equal.

type TestTuples = Equals<[param: string], [param: string]>;
//   ^ false
// Should be `true`.

type TestOptionalTuples = Equals<[param?: string], [param?: string]>;
//   ^ false
// Should be `true`.

export {};

declare global {
    interface Array<T> {
        // The names do not matter.
        // someMethod<O>(other: Extract<O, T>): [Extract<T, O>]; // Swapping out for this makes only optional tuples compare unequally.
        someMethod<O>(other: O extends T ? O : any): [T extends O ? any : any]; // This makes both optional and non-optional tuples compare unequally.
    }
}

🙁 Actual behavior

TestTuples and TestOptionalTuples are false whereas TestTypeAliasOptionalTuples and TestUnnamedTuples are true.

🙂 Expected behavior

For all of these tests to return true.

Additional information about the issue

While this someMethod merge is obviously contrived, I actually ran into this while typing a library that adds an Array.equals and Array.partition method.

After hunting it down I simplified the types to find this case.

@jcalz
Copy link
Contributor

jcalz commented Feb 11, 2025

Not a TS team member.

Looks like the TS team isn't really interested in supporting that Equals check, which probes implementation details of TS:
#57918 (comment)
#58163 (comment)

Two types that pass the Equals check are almost certainly equal, but two types that fail the check might well be "equal", they might just not share a single internal reference. (Like how in JavaScript one should not be surprised that, say, {} !== {}.) Maybe if there's some trivial fix that doesn't degrade compiler performance they'd accept it, but generally speaking they'd mostly just discourage you from using that check in the first place.

@LukeAbby
Copy link
Author

While I'm sympathetic to that point of view, the issue with the other mutual assignability test defined as type Equals<T, U> = [T, U] extends [U, T] ? true : false is that it has a number of adverse cases:

Equals<{}, { x?: number }> // true
Equals<any, 1> // true
Equals<{}, object> // true
Equals<void | undefined, void> // true

When a type testing library is involved I'd say you probably don't want any of those to count as equal. If you want to deal with them what you'll need to do is do a recursive walk of the type (properly bailing on loops in the type!) and looking for these edge case types and then special casing them.

While not impossible they're definitely not performant either.

@jcalz
Copy link
Contributor

jcalz commented Feb 12, 2025

So then maybe we want upvotes and feedback on #48100 asking for a better type level equality operator, since neither mutual assignability nor reference equality does the trick, and user-defined versions are complex and slow. Maybe an argument could be made there that in the absence of a reasonable native operator, too many people are choosing to use an unsupported/unsupportable approach.

I thought the main problem is that there are too many edge cases where people disagree on what equality is.

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Feb 12, 2025
@typescript-bot
Copy link
Collaborator

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

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Feb 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

4 participants