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

Explicitly Omit unspreadable properties from rest type in the generic case #47078

Merged
merged 9 commits into from
Dec 9, 2021

Conversation

jakebailey
Copy link
Member

@jakebailey jakebailey commented Dec 9, 2021

Fixes #46967

In the non-generic case, we'd produce a concrete type that consisted only of the spreadable properties, excluding already destructured and unspreadable properties. This is good.

However, in the generic case (including this, as it's an implicit type parameter), we instead produced an Omit type, but only omitted the already destructured keys. This left a type where all of the unspreadable properties (e.g. properties with getters, methods) where left accessible.

Make the generic case match the behavior of the non-generic case by also adding the unspreadable items to the omitted set of keys.

This might get pretty hairy for types with lots of getters/setters/methods, though, but the alternative (making a concrete type instead if it looks like Omit can't handle it) appears to break a few tests in even more undesirable ways. Hopefully destructuring this with a rest is not all that common (the user tests imply it is not).

Additionally, I don't know if the behavior on non-public properties is correct; it seems weird that destructuring this within the class would only keep the public properties, even though at runtime they are there. They're dropped in the T extends SomeClass case because that's a use where those symbols are not intended to be visible, but internally, maybe it's not right to drop them.

Before: Playground Link

After: Playground Link

@typescript-bot typescript-bot added Author: Team For Milestone Bug PRs that fix a bug with a specific milestone labels Dec 9, 2021
@jakebailey
Copy link
Member Author

@typescript-bot pack this
@typescript-bot user test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 9, 2021

Heya @jakebailey, I've started to run the parallelized community code test suite on this PR at f2bb9b7. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 9, 2021

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

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 9, 2021

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/116151/artifacts?artifactName=tgz&fileId=1CE0D0A5586337F8919658F9353500694094A4FAFF636CE88EB6AC06AB9AC66802&fileName=/typescript-4.6.0-insiders.20211209.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]".;

@typescript-bot
Copy link
Collaborator

The user suite test run you requested has finished and failed. I've opened a PR with the baseline diff from master.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Dec 9, 2021

Additionally, I don't know if the behavior on non-public properties is correct; it seems weird that destructuring this within the class would only keep the public properties, even though at runtime they are there.

This is good; we're "allowed" to have types which don't represent the full list of properties present at runtime (this is the same logic by which A is assignable to Record<keyof A, unknown>), and access to private/protected members is only supposed to be allowed through references of the actual class type from within the class (whereas these object types can "leak" out in a bunch of ways). Having a spread inside the class work the same as a spread outside the class (and not have properties which aren't there) is the more important bit.

@jakebailey
Copy link
Member Author

Having a spread inside the class work the same as a spread outside the class (and not have properties which aren't there) is the more important bit.

Good, less work for me! 😄

@jakebailey
Copy link
Member Author

Updated the PR to also test protected, now that I know that my behavior is correct.

@jakebailey jakebailey merged commit 0ed9247 into microsoft:main Dec 9, 2021
@jakebailey jakebailey deleted the fix-46967 branch December 9, 2021 19:45
mprobst pushed a commit to mprobst/TypeScript that referenced this pull request Jan 10, 2022
@DanielRosenwasser DanielRosenwasser added the Breaking Change Would introduce errors in existing code label Jan 19, 2022
@DanielRosenwasser
Copy link
Member

This is less about Omit and more about all mapped object types right?

@jakebailey
Copy link
Member Author

Yes, this is related to property spreading of a value with unspreadable properties; Omit is just the method we use when the thing being spread is generic (when concrete, we just make a new object type).

@kasperpeulen
Copy link

@jakebailey But this still breaks on runtime if you uses interface that are implemented with a class, right?

interface Thing {
  someProperty: number;

  someMethod(): void;
}

function foo<T extends Thing>(x: T) {
  let { someProperty, ...rest } = x;

  // Not an error, but will break on runtime with: foo(new MyThing());
  rest.someMethod();
}

class MyThing implements Thing {

    someProperty = 42;

    someMethod() {
        // ...
    }

}

// fine
foo({ someProperty: 1, someMethod: () => { } });

// breaks on runtime
foo(new MyThing());

@jakebailey
Copy link
Member Author

jakebailey commented Feb 16, 2022

Sure, but that code doesn't error before this PR too. I'd file a new issue for that (though there's probably an issue somewhere about how these sorts of own property semantics aren't fully modeled).

@Beraliv
Copy link

Beraliv commented May 2, 2022

Hello everyone!

Thank you for your changes @jakebailey, I really appreciate it!

I have one question out of curiosity: if I have a constructor where I bind methods, they should be allowed for the rest type, right? At the moment I see error that Property 'someMethod' does not exist on type 'Omit<T, "someProperty" | "someMethod">'

Playground – https://tsplay.dev/mMMebm

Example
class Thing {
  someProperty = 42;

  constructor() {
      this.someMethod = this.someMethod.bind(this);
  }

  someMethod() {
      // ...
  }
}

function foo<T extends Thing>(x: T) {
  let { someProperty, ...rest } = x;

  // Used to work, is now an error!
  // Property 'someMethod' does not exist on type 'Omit<T, "someProperty" | "someMethod">'.
  rest.someMethod();
}

Should I create a separate issue for it? @RyanCavanaugh

@jakebailey
Copy link
Member Author

I would file a new issue (as this is the wrong place to discuss it), but I'm not sure that we do anything special for a method bound like that. Check out the d.ts file output given in your example; the method's type is identical whether or not the constructor reassigns it.

This is no different than how we've always behaved in the non-generic case: Playground Link

This PR made things consistent between the two.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team Breaking Change Would introduce errors in existing code For Milestone Bug PRs that fix a bug with a specific milestone
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Using rest destructing on class objects seems to include getters in the resulting type
6 participants