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
Improve computed base constraint for Index type on generic IndexedAccess #49107
base: main
Are you sure you want to change the base?
Improve computed base constraint for Index type on generic IndexedAccess #49107
Conversation
60b80b8
to
19aa7d1
Compare
This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise. |
19aa7d1
to
6ba4afd
Compare
src/compiler/checker.ts
Outdated
const baseIndexType = getBaseConstraint(indexedAccess.indexType); | ||
const indexedAccessType = baseObjectType && baseIndexType && getIndexedAccessTypeOrUndefined(baseObjectType, baseIndexType); | ||
const mappedIndexTypeOfIndexedAccess = indexedAccessType && mapType(indexedAccessType, getIndexType); | ||
const narrowed = mappedIndexTypeOfIndexedAccess && mapType(keyofConstraintType, t => { |
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.
const narrowed = mappedIndexTypeOfIndexedAccess && mapType(keyofConstraintType, t => { | |
const intersected = mappedIndexTypeOfIndexedAccess && getIntersectionType([keyofConstraintType, mappedIndexTypeOfIndexedAccess]); | |
const narrowed = intersected.flags & TypeFlags.Never ? undefined : intersected; |
should be equivalent and much simpler.
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.
IIRC I intentionally haven't written it like that - although rn I don't exactly remember why.
Those are not equivalent as my version filters each member of the keyofConstraintType
individually, so I end up with a subset of keyofConstraintType
. Yours simply intersects keyofConstraintType
with mappedIndexTypeOfIndexedAccess
and that will preserve subset of the mappedIndexTypeOfIndexedAccess
. So the return types of those operations are not the same.
As I said - right now I don't recall right now why I've chosen this approach but I remember that it was on purpose. That, of course, doesn't mean that my version is better here - perhaps yours is.
I feel like there is probably a test case to be written here that would help us to determine which one is correct...
Should this be assignable?
export type Context = {
V1: { "a": string },
V2: { "b": string },
}
export function path<V extends keyof Context, K extends keyof Context[V]>(v: V, k: K) {
// should this be assignable?
const a: 'a' | 'b' = k
return a
}
With my version of the code it isn't but with yours it is.
Funny enough this indexed access errors with both versions:
export type Context = {
V1: { "a": string },
V2: { "b": string },
}
declare const c: {
a: string;
b: string
}
export function path<V extends keyof Context, K extends keyof Context[V]>(v: V, k: K) {
const a: 'a' | 'b' = k
return c[k] // this errors
}
So it probably should be fixed too if your proposed change is correct.
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, I think that your version wouldn't be correct - mainly because of the presented case with this being assignable:
const a: 'a' | 'b' = k
We can't guarantee this. After all, as K
might only be instantiated with 'a'
, 'b
or never
. Those are the only (?) valid calls here:
path('V1', 'a')
path('V2', 'b')
declare const v: keyof Context
path(v, {} as never)
keyof
is not distributable so keyof ({ a: string } | { b: string })
is simply never as it gets the common set of properties.
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.
keyofConstraintType
is string | number | symbol
, afaik if mappedIndexTypeofIndexedAccess
is T
, the operation here is essentially (string & T) | (number & T) | (symbol & T)
, which is fundamentally what T & (string | number | symbol)
works out to, no? Unless I'm missing something very subtle?
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.
My operation is closer to this:
type Process<T1, T2> = T1 extends any
? [T1 & T2] extends [never]
? never
: T1
: never;
type FilteredSource = Process<PropertyKey, "a" | "b">;
// ^? string
type Intersected = PropertyKey & ("a" | "b");
// ^? 'a' | 'b
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.
Ah, I missed that it was returning t
and not the intersection result in the non-never case.
You wanna use filterType
instead of mapType
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.
You wanna use filterType instead of mapType then.
I've pushed out this change.
q: do you maybe have an idea for a better test than the first snippet here? or do you think that trying to write a "reversed" test here is not worth it/bad idea?
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.
@weswigham friendly 🏓 do you believe this is a good approach for this problem or would you like me to explore some alternative solutions?
@MartinJohns thanks for mentioning this PR in one of the issues. I've noticed that and thanks to that I've managed to correct the PR. |
…int-computation-on-generic-indexed-type
@jakebailey would there be a chance for a playground build here? :) |
@typescript-bot pack this |
Heya @jakebailey, I've started to run the tarball bundle task on this PR at cfcfa9f. 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 |
…int-computation-on-generic-indexed-type
Relates to the issue reported here: #49071
This is what I got here... from what I understand
K
is constrained to be of typekeyof Context[V]
and that stays unresolved~ becauseV
is generic. So it stays to be represented as an Index type (which is a "return type" of thekeyof
operator from what I understand). And as such TS computes its base constraint like this here:If I reason about this correctly - we can't just compute the base constraint as a union of those inner properties because that would possibly allow some invalid property accesses within the function's body. If we'd compute this indexed access then this constraint would be roughly equivalent to
keyof ({ a: string; } | { b: string; })
but that's justnever
.So I've sketched out this algorithm for the improved computation - but I'm not sure if it's completely correct.
cc @dragomirtitian