Skip to content

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

Conversation

Andarist
Copy link
Contributor

fixes #37879

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Apr 16, 2023
@@ -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) :
Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

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 👍

@@ -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) :
Copy link
Contributor

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.

@Andarist Andarist requested a review from rbuckton April 27, 2023 22:12
@Andarist
Copy link
Contributor Author

@sandersn hmm, it feels that the automation didn't move this to the "waiting on reviewers" column

@sandersn
Copy link
Member

@Andarist yep, that part isn't automated yet

@Andarist
Copy link
Contributor Author

Andarist commented Jun 9, 2023

@rbuckton @gabritto friendly 🏓


verify.quickInfoAt("1", "(property) A.arr: string[]");
verify.quickInfoAt("2", "(property) Foo.bar: {\n baz: string;\n}");
verify.quickInfoAt("3", "(property) baz: string | undefined");
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

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 😄

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@rbuckton rbuckton left a 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.

@DanielRosenwasser
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jun 13, 2023

Heya @DanielRosenwasser, I've started to run the tarball bundle task on this PR at d107980. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jun 13, 2023

Hey @DanielRosenwasser, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/155473/artifacts?artifactName=tgz&fileId=6BF06C37B292DD6D02F58922E5CF285D242DCFAE403BD8E407538BD961555EBF02&fileName=/typescript-5.2.0-insiders.20230613.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/[email protected]".;

@@ -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));
Copy link
Member

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?

Copy link
Contributor

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.

Copy link
Contributor

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.

@rbuckton
Copy link
Contributor

@DanielRosenwasser, @jakebailey any further feedback? If not, I will merge this.

@DanielRosenwasser
Copy link
Member

I agree that my point doesn't need to hold back the PR

@rbuckton rbuckton merged commit 39da6b1 into microsoft:main Jun 23, 2023
@ghost
Copy link

ghost commented Jun 30, 2023

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 Array<String>.length can still only be number (as in test3).
The tooltip also shows the explanation for the length function, but for test and test2 one can get undefined which isn't explained in the description for length.
Again, the origin of undefined is not the property, but the expression it's used in.

Alternatively, an option to display it would be along the lines of (property) undefined | Array<String>.length: number (which shows that it's either undefined before reaching Array<String>).
I think this could also reveal potential ambiguity for undefined like (property) undefined | CustomClass.customFn: number | undefined.
Some other variation instead of the undefined | prefix could also be possible.

Edit: I assume this is what https://github.com/microsoft/TypeScript/pull/53804/files#r1224576390 was about.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Hover over property accessed using ?. shows T | undefined
7 participants