Skip to content
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

Skip parent error when reporting excess property checks #55152

Conversation

RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Jul 25, 2023

When we report an excess property error, the current error chain looks like this

    Argument of type '{ z: number; a: number; }' is not assignable to parameter of type 'T'.
      Object literal may only specify known properties, and 'z' does not exist in type 'T'.

As frequently noted by @fatcerberus, this gives the misleading impression that object types are sealed, since the way we printback a fresh object literal type is indistinguishable from a regular object type. It's also a waste of space since the top errorline prints back T when it's already in the EPC line, and the object literal source type is always anonymous (therefore long).

This PR skips the parent line when reporting EPC errors, so you don't see the "x is not assignable to y" part. The implementation is set up to handle the tricky case

declare function fn(a: { x: string }): void;
declare function fn(a: { y: string }): void;
fn({ z: 3, a: 3 });

where we want to print back two EPC errors:

// BEFORE
No overload matches this call.
  Overload 1 of 2, '(a: { x: string; }): void', gave the following error.
    Argument of type '{ z: number; a: number; }' is not assignable to parameter of type '{ x: string; }'.
      Object literal may only specify known properties, and 'z' does not exist in type '{ x: string; }'.
  Overload 2 of 2, '(a: { y: string; }): void', gave the following error.
    Argument of type '{ z: number; a: number; }' is not assignable to parameter of type '{ y: string; }'.
      Object literal may only specify known properties, and 'z' does not exist in type '{ y: string; }'.

// AFTER
No overload matches this call.
  Overload 1 of 2, '(a: { x: string; }): void', gave the following error.
    Object literal may only specify known properties, and 'z' does not exist in type '{ x: string; }'.
  Overload 2 of 2, '(a: { y: string; }): void', gave the following error.
    Object literal may only specify known properties, and 'z' does not exist in type '{ y: string; }'.

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Jul 25, 2023
# Conflicts:
#	tests/baselines/reference/jsxSpreadTag.js
#	tests/baselines/reference/jsxSpreadTag.symbols
#	tests/baselines/reference/jsxSpreadTag.types
#	tests/cases/compiler/jsxSpreadTag.ts
@@ -20354,6 +20354,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let overrideNextErrorInfo = 0; // How many `reportRelationError` calls should be skipped in the elaboration pyramid
let lastSkippedInfo: [Type, Type] | undefined;
let incompatibleStack: DiagnosticAndArguments[] | undefined;
let skipParentCounter = 0;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently never goes higher than 1, but if we ever had the notion of an EPC->EPC nested error, that would be how it could happen. Open to changing it to a boolean if wanted.

@RyanCavanaugh
Copy link
Member Author

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 26, 2023

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

@typescript-bot
Copy link
Collaborator

Hey @RyanCavanaugh, 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/156034/artifacts?artifactName=tgz&fileId=B26DD9E3815050662BE8EF1C000273AF87EDCE92F308C8953CF9FBCAE11CB8DC02&fileName=/typescript-5.2.0-insiders.20230726.tgz"
    }
}

and then running npm install.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Jul 26, 2023

Let's make sure this example doesn't regress and always reports on the discriminant.

export type Blah =
    | { type: "foo", abc: string }
    | { type: "bar", xyz: number, extra: any };

declare function thing(blah: Blah): void;

let foo = "foo";
thing({
    type: foo,
    abc: "hello!"
});

thing({
    type: foo,
    abc: "hello!",
    extra: 123,
});

let bar = "bar";
thing({
    type: bar,
    xyz: 123,
    extra: 123,
});

Comment on lines -13 to +11
!!! error TS2352: Object literal may only specify known properties, and 'foo' does not exist in type '{ id: number; }'.
!!! error TS2352: Object literal may only specify known properties, and 'foo' does not exist in type '{ id: number; }'.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error should never have been issued in the first place. It seems like a bug in our element-wise elaboration logic. Notice how in the following example one of these is has a missing property error and the other is an excess property error:

let okay = { foo: "" } as { id: 123 };
//         ~~~~~~~~~~~~~~~~~~~~~~~~~~
// Conversion of type '{ foo: string; }' to type '{ id: 123; }' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
//   Property 'id' is missing in type '{ foo: string; }' but required in type '{ id: 123; }'.

let waat = [{ foo: "" }] as { id: 123 }[];
//            ~~~
// Conversion of type '{ foo: string; }[]' to type '{ id: 123; }[]' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
//   Type '{ foo: string; }' is not comparable to type '{ id: 123; }'.
//     Object literal may only specify known properties, and 'foo' does not exist in type '{ id: 123; }'.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened up #55167

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wouldn’t ever expect an EPC error on a type assertion.

@jakebailey
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 26, 2023

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

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 26, 2023

Hey @jakebailey, 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/156037/artifacts?artifactName=tgz&fileId=01D663BA71564B95BE70D54F83FD6EDFC891B4A2C2450DE7DEDF4C0FA2FE37F702&fileName=/typescript-5.2.0-insiders.20230726.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/pr-build@5.2.0-pr-55152-6".;

@RobertSandiford
Copy link

Suggestions on wording from a user who was previously confused by it

Priorities:

  1. Indicate that the error is a sanity check, not a type violation.
  2. Indicate that the object literal can be assigned to that variable, albeit not directly.
  3. Show the user how to assign it, if there is a preferred way.

Warning: Property 'z' does not exist in type 'T'. Is this a mistake? To continue, cast the object literal to type 'T' or 'typeof var/Parameters<typeof func>[n]*', or save it to a new variable, before assignment.

  • Delete as appropriate. Do you have access to the variable/function name and arg pos?

It's a bit tricky to give clear instructions on how to assign it given the many scenarios. Simpler option might be:

Warning: Property 'z' does not exist in type 'T'. Is this a mistake? Can't assign this object literal directly, to continue, use a typecast, assign to an intermediate variable, or use an IIFE.

Really the important thing isnt how to assign but that the user understands that it is possible, to avoid misundersranding types.

@RyanCavanaugh
Copy link
Member Author

RyanCavanaugh commented Jul 31, 2023

We can discuss here but I'd like to change the message (if we do) in a different PR

Whether or not the error message should describe how to disable the error is a bit of a judgment call. Most errors today don't do this; usually we only do this when the error is low-confidence. EPCs are, in my experience, very high-confidence -- it's nearly always a typo or total user error to have an excess property, and the brevity of the error message can reflect this.

Going off a concrete example

type Dimensions = {
    width: number;
    height: number;
}
const m: Dimensions = { width: 0, length: 0, height: 0 };

The current error stack (as of this PR) is just

Object literal may only specify known properties, and 'length' does not exist in type 'Dimensions'.

I liked the phrasing we switched to in #50471, where we're pointing out that something seems wrong rather than declaring some impossibility. Applying that style here, I'd maybe write something like

Property 'length' appears unintentional because no matching property exists in 'Dimensions'.

Advantages

  • "Appears unintentional" makes it clear this is just a "Hey something is wrong" check
  • Shorter than the current phrasing
  • Puts the relevant info (property name) closer to the start of the message

We also have the typo-detected variant:

type Dimensions = {
    width: number;
    height: number;
    length?: number;
}
const m: Dimensions = { width: 0, lenghh: 0, height: 0 };

Object literal may only specify known properties, but 'lenghh' does not exist in type 'Dimensions'. Did you mean to write 'length'?

I would just shorten further:

'lenghh' appears to be a typo of property 'length' from type 'Dimensions'

@RobertSandiford
Copy link

@RyanCavanaugh Yeah I generally agree. No issues with going ahead with something along those lines.

I based my suggestion on the type casting error which is similar in feeling, but with a suggestion on how to fix it.
Conversion of type '{ c: number; }' to type 'O' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. Property 'a' is missing in type '{ c: number; }' but required in type 'O'.(2352)

I think in the situation where you want to add properties, you are creating an object that conforms to some type standard, that is probably defined externally.

For simply object creation, you could extend this type with an intersection to creating your own expanded type.
Alternatively you could use satisfies and let TS infer the type of the new object. I was surprised to find that satisfies checks are subject to EPC too, I expect them to be treated like type cast, but this can be worked around by pushing the satisfies check into the next statement.

The other scenario that I can think of is abstract properties, but the way that this is handled is currently very permissive, as previously discussed, so no EPC checking here.

Playground

So the only scenario where EPC would come into play here is the satisfies approach.

Just thought I would lay that out incase it helps anyone.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Aug 1, 2023

Property 'length' appears to be unintentional because no matching property exists in 'Dimensions'.

Property 'lengthh' appears to be unintentional because no matching property exists in 'Dimensions'. Did you mean 'length'?

I like those.

Nit: In the ones I quoted, I fixed them up to "appears to be unintentional"

@@ -20354,6 +20354,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let overrideNextErrorInfo = 0; // How many `reportRelationError` calls should be skipped in the elaboration pyramid
let lastSkippedInfo: [Type, Type] | undefined;
let incompatibleStack: DiagnosticAndArguments[] | undefined;
let skipParentCounter = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be captured in captureErrorCalculationState and reset in resetErrorInfo so it's set/unset when a speculative relationship elaboration pyramid is discarded. AFAIK, you'd need to do something like trigger EPC inside a conditional type or indexed access type where we end up using the constraint check rather than the normal check for validity for it to matter (and the bug would look something like us discarding elaboration layers for non-EPC errors). Definitely niche, but those functions are just there for capturing/restoring error pyramid state, so should also track this new variable.

Also, maybe a comment on how this differs from the nearby overrideNextErrorInfo number. (overrideNextErrorInfo skips the next N things in the pyramid, while when set this skips the N prior things in the pyramid.)

@fatcerberus
Copy link

this gives the misleading impression that object types are sealed, since the way we printback a fresh object literal type is indistinguishable from a regular object type.

Case in point. In general, people see "X is not assignable to Y" and expect that to apply in all contexts. That issue, incidentally, also highlights another way the current error is misleading: it says "object literal may only specify known properties", but many defect reports still involve object literals, just without the contextual typing required for EPC to work.

PR Backlog automation moved this from Not started to Needs merge Aug 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
PR Backlog
  
Done
Development

Successfully merging this pull request may close these issues.

None yet

7 participants