-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Fixed an issue with apparent mapped type keys #57838
base: main
Are you sure you want to change the base?
Fixed an issue with apparent mapped type keys #57838
Conversation
src/compiler/checker.ts
Outdated
@@ -22486,7 +22486,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||
/*stringsOnly*/ false, | |||
t => void mappedKeys.push(instantiateType(nameType, appendTypeMapping(targetType.mapper, getTypeParameterFromMappedType(targetType), t))), | |||
); | |||
return getUnionType(mappedKeys); | |||
return mappedKeys.length ? getUnionType(mappedKeys) : stringNumberSymbolType; |
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.
The problem here is that when modifiersType
is unknown
(or {}
) then keyof modifiersType
is never
. Since never
is assignable to everything, the checker assumed that keyof { [P in keyof T as T[P] extends string ? P: never]: any }
is assignable to string
.
Then based on that information it managed to simplify the instantiated substitution type of Extract
to just keyof { [P in keyof T as T[P] extends string ? P: never]: any }
(its newBaseType
). Then in turn getNarrowableTypeForReference
concluded that it should substitute constraints. Those were substituted with stringNumberSymbolType
(for the keyof ...
) and that's not assignable to string
.
Before #56742 , it didn't mistakenly assume that keyof { [P in keyof T as T[P] extends string ? P: never]: any }
is assignable to string
so the substitution type was instantiated as (keyof { [P in keyof T as T[P] extends string ? P: never]: any }) & string
and that is assignable to a string.
…ype-keys-with-unknown-modifier
Hi! I appreciate that reviewers can be very busy but I discovered this PR when bisecting my own issue with a bit of an absurd reproduction that was erroring with a much more opaque error "Type instantiation is excessively deep and possibly infinite." so I was unable to find any workarounds before the process of bisecting. I'd really like to see this PR merged because when I built it locally it makes my code work! I wouldn't want to ask this of anyone else without being willing to do it myself so I did also review it and I can't find anything objectionable, though of course my opinion doesn't weigh as much given I'm not a maintainer. |
…ype-keys-with-unknown-modifier
@typescript-bot pack this |
Hey @gabritto, 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 it |
@gabritto Here are the results of running the user tests with tsc comparing Everything looks good! |
Hey @gabritto, the results of running the DT tests are ready. Everything looks the same! |
@gabritto Here they are:
tscComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
Developer Information: |
|
||
function h<T>(z: StringKeys2<T>) { | ||
z = "foo"; // error |
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.
Maybe add a const f: string = z;
here too to show that it will still error?
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.
Shouldn't this be OK though? Only string keys are extracted by StringKeys2
so z
has a string constraint~ and is assignable to string
. This is how it works today (TS playground) and this PR doesn't change that
let sourceMappedKeys: Type; | ||
if (nameType && isMappedTypeWithKeyofConstraintDeclaration(mappedType)) { | ||
sourceMappedKeys = getApparentMappedTypeKeys(nameType, mappedType); | ||
if (sourceMappedKeys.flags & TypeFlags.Never) { |
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 find it a bit weird that:
keyof unknown
isundefined
.- the fix is looking at the result of
getApparentMappedTypeKeys
. I'd expect it to be insidegetApparentMappedTypeKeys
. Isn't there a case wheregetApparentMappedTypeKeys
isnever
andsourceMappedKeys
should stay asnever
?
This doesn't look wrong to me, but I can't yet form a good mental model of what's happening, so I don't know what to think. 😅
I'll revisit in a few days and maybe I'll have a better idea of what this all means.
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.
keyof unknown is undefined.
If we think about keyof T
as a set of types that can be safely used to access properties of T
in T[keyof T]
then never
makes sense here. never
is the only type that can be safely used as keyof T
after we instantiate T
as unknown
.
the fix is looking at the result of getApparentMappedTypeKeys. I'd expect it to be inside getApparentMappedTypeKeys. Isn't there a case where getApparentMappedTypeKeys is never and sourceMappedKeys should stay as never?
That's what I started with but mapping never
to PropertyKey
would break the other call site where getApparentMappedTypeKeys
is used. In there, it's used to get the target keys. So this would remove the error below:
type StringKeys2<T> = keyof {
[P in keyof T as T[P] extends string ? P : never]: any;
};
function h<T>(z: StringKeys2<T>) {
z = "foo"; // error
}
@gabritto Here are the results of running the top 400 repos with tsc comparing Everything looks good! |
2f2b500
to
cad532a
Compare
fixes #57827