-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Display property types in optional chains without optional type marker #53804
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
Display property types in optional chains without optional type marker #53804
Conversation
src/compiler/checker.ts
Outdated
@@ -27526,7 +27526,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||
location = location.parent; | |||
} | |||
if (isExpressionNode(location) && (!isAssignmentTarget(location) || isWriteAccess(location))) { | |||
const type = getTypeOfExpression(location as Expression); | |||
const type = location.flags & NodeFlags.OptionalChain ? | |||
checkPropertyAccessChain(location as PropertyAccessChain, CheckMode.Normal, /*wasOptional*/ false) : |
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.
Note that this might not contain some guard rails related to control flow analysis that are present in getTypeOfExpression
. I'm not sure if they are relevant for this case though.
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.
Wouldn't this work as well?
const type = removeOptionalTypeMarker(getTypeOfExpression(location as Expression));
For an expression like a?.b.c
, hovering over c
would still show C | undefined
(since its the end of the chain and the optional type marker is replaced with undefined
), but hovering over b
would just show B
.
The upside is that getTypeOfExpression
caches, so you avoid recomputing a type that's already cached.
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.
yeah, that works fine I think - I pushed out the requested change
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.
There's no need to make this conditional on location.flags & NodeFlags.OptionalChain
. The marker type will only be present for optional chains.
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.
cool, removed this redundant check 👍
src/compiler/checker.ts
Outdated
@@ -27526,7 +27526,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||
location = location.parent; | |||
} | |||
if (isExpressionNode(location) && (!isAssignmentTarget(location) || isWriteAccess(location))) { | |||
const type = getTypeOfExpression(location as Expression); | |||
const type = location.flags & NodeFlags.OptionalChain ? | |||
checkPropertyAccessChain(location as PropertyAccessChain, CheckMode.Normal, /*wasOptional*/ false) : |
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.
There's no need to make this conditional on location.flags & NodeFlags.OptionalChain
. The marker type will only be present for optional chains.
@sandersn hmm, it feels that the automation didn't move this to the "waiting on reviewers" column |
@Andarist yep, that part isn't automated yet |
|
||
verify.quickInfoAt("1", "(property) A.arr: string[]"); | ||
verify.quickInfoAt("2", "(property) Foo.bar: {\n baz: string;\n}"); | ||
verify.quickInfoAt("3", "(property) baz: string | 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.
I'm confused as to the end result here; both bar
and baz
themselves are not declared as potentially undefined, and yet bar
is not | undefined
while baz
is. Why should they behave differently?
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 admit this is a little bit confusing and I'm open for feedback how to improve this further.
baz
has | undefined
because it's the end of the optional chain expression and the whole optional chain expression has | undefined
. Without this bit you'd look at an if statement that would always~ be truthy. That | undefined
is here to inform you that the whole expression can indeed result in both branches to be taken.
OTOH, bar
doesn't have it because with it it looks really weird that we can just chain off this expression (foo?.bar
) without guarding the .baz
property access
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 guess that makes sense, though I'd probably personally defer to @DanielRosenwasser 😄
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 intuition described is fine, but this example kind of shows some inconsistencies in this approach where method return types don't exhibit the same behavior.
Whether that is enough of a blocker, I don't know.
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.
That's a good catch. I think that it's worth unifying those - I'd expect x?.method;
to include undefined
based on the given described intuition. I see how this is quite subtle though so perhaps not everybody would agree with me on this one and that's fine.
I think that this find doesn't have to block this PR, I can just prepare a follow up fixing it at some later point in time. Up to you though.
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 looks good, though I'll give @jakebailey time to reply with any additional feedback before merging.
…in-display-nonnullable-type
@typescript-bot pack this |
Heya @DanielRosenwasser, I've started to run the tarball bundle task on this PR at d107980. You can monitor the build here. |
Hey @DanielRosenwasser, 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 |
@@ -27609,7 +27609,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||
location = location.parent; | |||
} | |||
if (isExpressionNode(location) && (!isAssignmentTarget(location) || isWriteAccess(location))) { | |||
const type = getTypeOfExpression(location as Expression); | |||
const type = removeOptionalTypeMarker(getTypeOfExpression(location as Expression)); |
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'm not that familiar with this part of the code. Why doesn't this also apply to the entirety of a?.b.c
?
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 "optional type" marker is not added at the outermost PropertyAccessExpression
, only for inner optional chain parts. At the outermost PropertyAccessExpression
, we just union it with the normal undefined
type.
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.
See propagateOptionalTypeMarker
in checker.ts for the logic related to when the marker type is added vs a regular undefined
.
@DanielRosenwasser, @jakebailey any further feedback? If not, I will merge this. |
I agree that my point doesn't need to hold back the PR |
I believe this is still incorrect. Here's an extended example from the original issue: interface A {
arr: string[];
}
function test(a?: A): string {
return a?.arr.length ? "A" : "B"; // Hovering `length` shows "(property) Array<String>.length: number | undefined"
}
function test2(a: string[] | undefined) {
return a?.length // Hovering `length` shows "(property) Array<String>.length: number | undefined"
}
function test3(a: string[]) {
return a.length // Hovering `length` shows "(property) Array<String>.length: number"
} This feels wrong, because Alternatively, an option to display it would be along the lines of Edit: I assume this is what https://github.com/microsoft/TypeScript/pull/53804/files#r1224576390 was about. |
fixes #37879