-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Merge trivially mergeable intersection types for identity comparison #60726
base: main
Are you sure you want to change the base?
Merge trivially mergeable intersection types for identity comparison #60726
Conversation
This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise. |
f7b81cf
to
7c9d5fa
Compare
@typescript-bot test it |
Starting jobs; this comment will be updated as builds start and complete.
|
@RyanCavanaugh Here are the results of running the user tests with tsc comparing Everything looks good! |
Hey @RyanCavanaugh, the results of running the DT tests are ready. There were interesting changes: Branch only errors:Package: hasura
|
Will take a look at this this week |
@RyanCavanaugh Here are the results of running the top 400 repos with tsc comparing Something interesting changed - please have a look. Details
|
I've sent PRs for
|
Can't promise we'll get to this before the holidays, but we'll definitely discuss |
Please verify that:
Backlog
milestone (required) - No one has created an issue for this specifically, but it is a subset of a larger set of problems discussed in [Feature request]type level equal operator #27024 and is similar to Make enum literal type identical to the union of its values #60417.main
branchhereby runtests
locallyDoes not explicitly fix any particular issues I can find, but addresses use cases in the comments of #27024
TL;DR: this PR makes
{a: 1} & {b: 2}
compare "equal" to{a: 1; b: 2}
.Use case:
Type testing libraries want to be able to compare types that are intuitively equal. Examples of such libraries include expect-type and type-plus. These try to leverage type comparisons that use the underlying
isTypeIdenticalTo
function inchecker.ts
.isTypeIdenticalTo
has stronger guarantees than mutual assignability, like ensuring{a?: 1} != {}
and{readonly a: 1} != {a: 1}
.Lack of these guarantees leads to deficiences like
It's increasingly important to be able to trust explicit type annotations under isolated declarations.
Approach:
Unlike #60417, this PR does not achieve equality by changing how types are normalized. That's because we don't actually want to change how the type is represented. For example, if we were to normalize
{a: 1} & {b: 2}
to{a: 1; b: 2}
the following code would break:Instead this approach updates the logic of
isRelatedTo
to directly handle this case.Caveats
This PR only attempts to make "trivial cases" work. At a high level this includes only intersections of plain object literals. I'll try to explain why other cases are not covered. Also consult the new test cases I added as reference.
Supported - All intersection constituents are plain object literals
{a: 1} & {b: 2} & {c: 3}
=={a: 1; b: 2; c: 3}
Supported - Recursive comparisons
{a: {x: 1}} & {a: {y: 2}}
=={a: {x: 1; y: 2}}
Unsupported - Any objects have call or construct signatures
{a: 1} & {(): void}
!={a: 1; (): void}
This is not supported right now because merging call signatures is more complicated. You would have to ensure that the ordering is preserved since the intersection of call signatures is equivalent to function overloading, where order matters. A subset of simpler call signature cases could be supported, but it doesn't seem worth the complexity for only a half-baked solution.
Unsupported - Any objects have index signatures
{a: 1} & {[key: number]: 2}
!={a: 1; [key: number]: 2}
The main reason to not support this now is because recursive merging can be more complicated, e.g.
You can write
{[key: number]: {x: 1}} & {[key: number]: {z: 1}}
but writing{[key: number]: {x: 1}; [key: number]: {z: 1}}
isn't allowed!Unsupported - Intersection includes non-plain objects, notably generic type variables
T & {a: 1} & {b: 2}
!=T & {a: 1} & {b: 2}
The main reason to not support this is because the implementation just becomes complicated and causes more types to be instantiated. Also consider the type
{a: 1} & T & {b: 2}
. At first glance, you might think that it's ok to make this equal toT & {a: 1} & {b: 2}
and hence equal toT & {a: 1; b: 2}
, but it's not actually safe to reorder constituents of intersections. As mentioned above, changing the order of call signatures changes the semantics of the type:Like other cases, we could try to handle a simpler subset of cases, but it doesn't seem worth it.
Testing
This patch has been tested in Airtable's codebase and does not introduce any visible regressions. I added several test cases in this PR.
Note
This PR does not automatically make libraries like
expect-type
work. For example, this will still not work:Why? Because of the way that
expect-type
implements its equality check.Note the
(L & T)
and(R & T)
. These intersections contain generic type variables and hence fall under the caveat above. I do not know whyexpect-type
implements type equality this way. I mean I know that it probably copy-pasted the recipe from this thread, but it's unclear why the recipe is defined that way. My proposal for libraries likeexpect-type
to benefit from this PR is to simplify their implementation to:I don't know of a case that the former definition satisfies that the latter does not, so if anyone knows, feel free to chime in.