Skip to content

Declaration merging can break equality tests for named tuples #61162

Closed as not planned
@LukeAbby

Description

@LukeAbby

🔎 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Not a DefectThis behavior is one of several equally-correct options

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions