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

Cannot subclass class-like types with accessors. #40733

Closed
ewlsh opened this issue Sep 24, 2020 · 5 comments · Fixed by #41994
Closed

Cannot subclass class-like types with accessors. #40733

ewlsh opened this issue Sep 24, 2020 · 5 comments · Fixed by #41994
Labels
Fix Available A PR has been opened for this issue In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@ewlsh
Copy link

ewlsh commented Sep 24, 2020

TypeScript Version: 4.1.0-beta

Search Terms: accessor property subclass mapped types

Code

type Types = 'boolean' | 'unknown' | 'string';

type Properties<T extends { [key: string]: Types }> = {
    readonly [key in keyof T]: T[key] extends 'boolean' ? boolean : T[key] extends 'string' ? string : unknown
}

type AnyCtor<P extends object> = new (...a: any[]) => P

declare function classWithProperties<T extends { [key: string]: Types }, P extends object>(properties: T, klass: AnyCtor<P>): {
    new(): P & Properties<T>;
    prototype: P & Properties<T>
};

const Base = classWithProperties({
    x: 'boolean',
    y: 'string',
    z: 'unknown'
}, class Base {
});

class MyClass extends Base {
    constructor() {
        super();
    }

    // Errors with: 'x' is defined as a property in class 'Base & Properties<{ x: "boolean"; y: "string"; z: "unknown"; }>', but is 
    // overridden here in 'MyClass' as an accessor. (2611)
    get x() {
        return false;
    }
}

const mine = new MyClass();
const value = mine.x;

Expected behavior:

I can create subclasses of mapped types when the mapped type defines a property and the subclass defines an accessor.

Actual behavior:

TypeScript gives an error saying you can't override a property with an accessor.

Playground Link: Link

Related Issues: I know this is related to the change in behavior in 4.0 to universally disallow accessors overriding properties.

@ewlsh
Copy link
Author

ewlsh commented Sep 24, 2020

This could also potentially be a feature request, ideally I'd like to be able to somehow declare getters/setters in a mapped type but I haven't been able to figure out a way to do it.

For now, I'm trying to type a JavaScript function which defines "properties" on a class (in JS it uses defineProperty with getters/setters) and then have other JavaScript classes subclass these types.

In previous versions, this worked but with the universal change to error in this case it doesn't.

I think it makes sense to error, but there is now no way (as far as I can tell) to use getters/setters with mapped types.

@sandersn
Copy link
Member

One almost-workaround is to use interface merging:

type BaseInterface = typeof Base;
interface MyClass extends BaseInterface { }
class MyClass {
}

But you can't call super anymore, and Base isn't emitted into JS in class MyClass extends Base, so I'm not sure it actually helps.

@ewlsh
Copy link
Author

ewlsh commented Sep 24, 2020

Hi @sandersn!

Unfortunately I'm trying to type a JS function which is used both in checkJS code and TypeScript code.

Also, in my more complicated version classWithProperties-modified classes can extend other classes which were modified with classWithProperties.

Specifically, we're working on bringing type checking to GNOME Shell (the desktop environment for Ubuntu, Fedora, etc.) and I'm writing a type definition for GObject.registerClass like in this example or here in a subclass of that class.

This is the actual work which is appended to this file.

Ideally there would be someway to describe getters/setters in the type syntax (I assume internally there must be some sort of flag for how a property is represented)...

But for now I really wish there was a way to disable this new behavior when classes extend from newable-types (and not class definitions).

@ewlsh
Copy link
Author

ewlsh commented Sep 24, 2020

This is (in some ways) the opposite problem of #40220.

@andrewbranch andrewbranch added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Sep 24, 2020
@sandersn
Copy link
Member

sandersn commented Dec 15, 2020

Looking again at this, there isn't a way, as far as I can tell, for object types to contain accessors. We should probably allow this on grounds that (1) accessors are not allowed, so the compiler can't know whether an accessor was intended (2) inheriting from object types is rare, happening mostly with mixins where we have real trouble representing the truth already.

Edit: I missed that this was coming from an object literal, not an object literal type. These do allow accessors, but the flag doesn't survive through the mapped type. It probably should.

However, this wouldn't solve the original problem, because the properties (like x: 'boolean') are not declared as accessors in the base.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Fix Available A PR has been opened for this issue In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants