-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Infer intersected reverse mapped types #52062
Conversation
9e6c3e1
to
ef2fba1
Compare
This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise. |
src/compiler/checker.ts
Outdated
return substituteIndexedMappedType(mappedType, propertyNameType); | ||
} | ||
}); | ||
return newTypes.length ? getIntersectionType(newTypes) : undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will quietly elide intersection members whose substitution fails the isTypeAssignableTo(propertyNameType, constraintOfConstraint)
check. I honestly can't think of how that could occur offhand, but I think to better mimic old behavior, since intersections are structured types, and would previously only take the other branch. If newTypes.length !== (t.flags & TypeFlags.Intersection ? (t as IntersectionType).types : [t]).length
, we need to fall through to the StructuredType
fallback below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried a couple of things to hit such a case and I can't think of any. I can introduce the proposed change but personally I think that it should be accompanied with a test case. I understand the desire to match the old behavior but:
- if we manage to grab a concrete property in the branch responsible for structured types then this is basically the same thing as substituting indexed mapped type (so doing it within this branch responsible for generic mapped types leads to the same results already)
- if we manage to grab some index info there then I find it extremely unlikely there is a way to reach that branch of code if all intersected mapped type failed
isTypeAssignableTo(propertyNameType, constraintOfConstraint)
check. In a lot of situations, theconstraintOfConstraint
is alreadystring | number | symbol
and even when it's not... the index type has to come from one of the intersected members, so some of them had to pass thisisTypeAssignableTo
check and thusnewTypes
wouldn't be empty. - The
isTupleType
is just impossible to be hit by intersections
So, all in all, I think that the requested change wouldn't be meaningful - but, ofc, I could be wrong here. And even if such intersections could today return something meaningful then that, I think, would be inconsistent with the isGenericMappedType(t) && !t.declaration.nameType
branch. That's because that branch doesn't fallback to the branch related to structured types - even if isTypeAssignableTo(propertyNameType, constraintOfConstraint)
. So if this doesn't happen for generic mapped types - why should it happen for intersected ones? That would be an inconsistent behavior to me.
@jakebailey could you prepare a playground for this one? 🙏 :) |
@typescript-bot pack this |
Heya @jakebailey, I've started to run the tarball bundle task on this PR at 25af013. You can monitor the build here. |
Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build and an npm module you can use via |
@typescript-bot test this |
Heya @weswigham, I've started to run the parallelized Definitely Typed test suite on this PR at 56f12bc. You can monitor the build here. Update: The results are in! |
Heya @weswigham, I've started to run the diff-based top-repos suite on this PR at 56f12bc. You can monitor the build here. Update: The results are in! |
Heya @weswigham, I've started to run the extended test suite on this PR at 56f12bc. You can monitor the build here. |
Heya @weswigham, I've started to run the perf test suite on this PR at 56f12bc. You can monitor the build here. Update: The results are in! |
src/compiler/checker.ts
Outdated
if (isTypeAssignableTo(propertyNameType, constraintOfConstraint)) { | ||
return substituteIndexedMappedType(t, propertyNameType); | ||
} | ||
if (everyContainedType(t, t => isGenericMappedType(t) && !t.declaration.nameType)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, thinking about it, shouldn't we just do this for each isGenericMappedType(t) && !t.declaration.nameType
within an intersection, and not only do this substitution if every member is such? (And still do the other StructuredType
behaviors for other intersection members). This way a Results<T> & Errors<E> & {x?: string}
still works.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, so I actually... have another PR that does exactly that 🤣 This one here is intentionally conservative when it comes to the scope of the change, especially since some other changes (outside of getTypeOfPropertyOfContextualType
) had to be made to support this use case. So it's not like that other PR completely replaces this one.
The PR in question is this: #52095 . Actually, one version of it was already merged into 4.8-beta but it was reverted cause @reduxjs/toolkit
regressed with it. However, it only regressed because of how I've handled intersections of types with concrete properties with types with index signatures.
In the original version of this PR (#48668) I decided to prefer concrete properties over index signatures, so (IIRC) if one of the intersected members had a concrete property for the requested one then index signatures on other intersected members were skipped over. This wasn't strictly needed to fix the issue that I was fixing but it was something that made sense to me at the time.
Either way... if you could take a look at that other PR (#52095), I would highly appreciate it. There is some funky stuff going on there with index signatures and that part is not correct - the problem is that I'm not sure how to adjust it since I'm not sure what exact effect are we aiming for there. Mixing index signatures with concrete properties through intersections is weird (like { foo: number } & { [key: string]: boolean }
). This particular place in the code doesn't exactly deal with concrete types either... so perhaps a guiding principle should be that properties should be pulled from types that can still resolve to concrete properties? I'm not sure how checking that would look like though - so perhaps we should just ignore it and always pull from index signatures?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmmmm, I'd rather not take this one with a known drawback like that - maybe it's best to just merge the two PRs, then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To keep the spirit of minimal changes I would probably prefer to keep them separate (if you prefer to merge them, then I won't oppose it though, at the end of the day I'd like both improvements to find their way into the language :P). I don't mind this one staying on the sidelines and waiting for the other one to land though. I would love for the other one to get some traction in terms of being reviewed 😜
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the common improvements need to be on both PRs (and the conflicts can be resolved later), or both PRs should be merged, yeah.
Hey @weswigham, the results of running the DT tests are ready. |
@weswigham Here are the results of running the top-repos suite comparing Everything looks good! |
@weswigham Here they are:
CompilerComparison Report - main..52062
System
Hosts
Scenarios
TSServerComparison Report - main..52062
System
Hosts
Scenarios
StartupComparison Report - main..52062
System
Hosts
Scenarios
Developer Information: |
Superseded by #52095 |
This doesn't exactly fix the code reported in #52047 but it makes it possible to rewrite the whole thing with intersected mapped types.
I still think that the better solution would be the one that I've proposed here but that doesn't mean that intersections like this can't work. Those are orthogonal capabilities - it's a different thing to infer a single type param based on multiple "partials" and it's a different thing to infer multiple type params like here. Both can co-exist.
The reverse mapped types are an incredibly useful technique. They allow us to infer types for something (like a callback's argument) based on a type in another property of the same tuple element (or within the same property value of a mapped object).
So for example this works (TS playground):
Unfortunately, they fall short today when trying to introduce more "relationships" like this for a given element~. Allowing to infer type params based on intersected mapped types would essentially lift this limitation and enable people to rewrite some crazy types with a much simpler technique (I'm specifically thinking here about very popular React Query and Redux Toolkit but the technique could be utilized in a lot of ways).