Skip to content

Allow this parameter in property accessors (getter/setter) #52923

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

Open
5 tasks done
jdatskuid opened this issue Feb 22, 2023 · 8 comments
Open
5 tasks done

Allow this parameter in property accessors (getter/setter) #52923

jdatskuid opened this issue Feb 22, 2023 · 8 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@jdatskuid
Copy link

Suggestion

Allow the this parameter in property accessors:

const extension = {
    get bar(this:ObjectWithFoo):string {
        return this.foo;
    }
}

πŸ” Search Terms

getter setter accessor this argument parameter

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
    As with functions, the this parameter for accessors is optional and would default to the home object.

  • This wouldn't change the runtime behavior of existing JavaScript code
    As with most of TypeScript, this proposal has no effect on emitted JavaScript.

  • This could be implemented without emitting different JS based on the types of the expressions
    As demonstrated below, this proposal technically already emits functioning JavaScript. This proposal only affects type checking (as with functions).

  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
    This proposal only affects type checking during compilation.

  • This feature would agree with the rest of TypeScript's Design Goals.
    While this feature agrees with all of the design goals, it fits most neatly under goals 5 and 9: "Produce a language that is composable and easy to reason about." and "Use a consistent, fully erasable, structural type system."

⭐ Suggestion

It is currently possible to assign a this parameter to a method to ensure that it is used correctly. For example:

interface ObjectWithFoo {
    foo: "foo";
};

const extensions = {
    baz(this:ObjectWithFoo):string {
        return this.foo + "baz";
    },
};

const target:ObjectWithFoo = {
    foo: "foo",
};

const extendedTarget = Object.assign(target, extensions);

extendedTarget.baz(); // "foobaz"

This capability is enormously useful when defining objects that will be used to extend other types.

However, there is a glaring missing feature. While it is possible to add property accessors to the extension object, they do not have access to the this reference, even though the getter and setter accessors are fundamentally just the get and set functions on the property descriptor. This is severely limiting, even though it is fully supported in JavaScript.

const extensions = {
    // The following accessor currently produces:
    //     Error: 'get' and 'set' accessors cannot declare 'this' parameters.(2784)
    get bar(this:ObjectWithFoo):string {
        return this.foo;
    },
    baz(this:ObjectWithFoo):string {
        return this.foo + "baz";
    },
};

πŸ“ƒ Motivating Example

Consider the following simple extension framework code. It uses getOwnPropertyDescriptor and defineProperty, which preserves the original accessor functions (the getter/setter). JavaScript evaluates the this references in the accessors in the context of the target object, allowing any property getter/setter logic to behave as if the properties were defined on the target from the beginning.

function extend<T, E>(target:T, extension:E): T & E {
    // Please note that this function is offered as a reasonable stand in for an extension framework.
    // It is not, on its own, a complete solution that handles all edge-cases.
    for (const extensionName in extensions) {
        const extensionDescriptor = Object.getOwnPropertyDescriptor( extensions, extensionName );
        if (!extensionDescriptor) throw Error("Missing property descriptor for: " + extensionName);
        Object.defineProperty(target, extensionName, extensionDescriptor);
    }
    return target as T & E;
}

TypeScript's support for this on property accessors would allow extension framework authors to express this powerful feature of JavaScript and ensure correctness in their code.

interface ObjectWithFoo {
    foo: "foo";
};

const extensions = {
    // Proposed:
    get bar(this:ObjectWithFoo):string {
        return this.foo;
    },
    baz(this:ObjectWithFoo):string {
        return this.foo + "baz";
    },
};

const target:ObjectWithFoo = {
    foo: "foo",
};

const extendedTarget = extend(target, extensions);

extendedTarget.bar; // "foo"

In addition to the convenience of accessing the intended this reference, TypeScript could use the same kind of type safety for properties that it currently offers for functions.

// Current:
extensions.baz(); // The 'this' context of type '...' is not assignable to method's 'this' of type '...'

// Proposed:
extensions.bar; // The 'this' context of type '...' is not assignable to accessor's 'this' of type '...'

Checking the intended this reference for property accessors could be used to help prevent common mistakes, such as the following:

Object.assign(target, extensions);

Here, the engineer intended to attach the bar property accessor functions but accidentally triggered the evaluation of the property value instead. Instead of assigning a dynamic getter, the target received a new property with a simple, fixed value (or a potential runtime error).

A playground link which demonstrates the proposed code (and actually runs, despite the TS error!)

πŸ’» Use Cases

The use cases for the this parameter in property accessors includes all the use cases for the this parameter in functions, including:

  • Extension frameworks
  • Mixins
  • Class and property decorators

...and any other code where a property accessor defined on one object will ultimately be used by another.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Feb 22, 2023
@fatcerberus
Copy link

fatcerberus commented Feb 22, 2023

FWIW I don't think having this types on getters would prevent the Object.assign mistake. As far as the type checker is concerned your accessors are just plain old data properties, so that would need to change first before the compiler would be able to catch that error.

@jdatskuid
Copy link
Author

Related Question/bug report: #39254

@rgbui
Copy link

rgbui commented Feb 25, 2023

I'm currently working on a block editor, so don't ask me why blocks have so many distributed classes. You know that many distribution classes support method(this:Block), but get parameter(this:Block) will get an error.
#39254 This is a problem I encountered three years ago when I was working on a low code project, and the syntax is not supported now
This grammar used to be supported, but two years ago you changed the rules. Too many get parameters are written in the main Block class because the syntax is not supported.

export interface Block extends Block$Seek { }
export interface Block extends Block$Event { }
export interface Block extends Block$Anchor { }
export interface Block extends Block$LifeCycle { }
export interface Block extends Block$Operator { }
export interface Block extends Block$Board { }
export interface Block extends Mix { }
Mix(Block, Block$Seek, Block$Event, Block$Anchor, Block$Board, Block$LifeCycle, Block$Operator)

property accessors can be thought of as a kind of computation property, which is essentially the same as a method, so why do you support methods, but not property accessors

property accessors (getters/setters) and methods are in the same domain and are usually not separated. I have to break them up now because the grammar doesn't support them. Can you explain the reason why you don't support them? Because supporting this creates other problems. I think ChatGPT would approve Allow this parameter in property accessors (getter/ setters)

@fatcerberus
Copy link

I think ChatGPT would approve Allow this parameter in property accessors (getter/ setters)

what.

@RyanCavanaugh
Copy link
Member

Can you explain the reason why you don't support them?

All features start at minus 100

I think ChatGPT would approve

Please stop πŸ₯²

@fatcerberus
Copy link

I am absolutely begging people on GitHub to stop phrasing developer expectations in terms of what ChatGPT would want. Let's just not do this.

Wait, that's actually a thing now? I was so confused by this, it's just a total non-sequitur, I thought there was a joke I wasn't getting.

@matAtWork
Copy link

I have a very similar use case. The getters and setters are being passed in a "prototype" and Object.defineProperties are used to attach them to a target object later when the object is instantiated. In this case, the this is not the surrounding context of the prototype, but that of the object it has been copied too.

Also, it works with standard functions (where I can specify this via ThisType<>), and from the outside getters & setters are basically functions with predefined parameters and a syntactic short-cut to them being invoked (I know there are other features, but from a semantic PoV this is the essence). This is clear from Object.defineProperties, where any function can be specified as a getter or setter. Note that we are talking about updating the definition of the getter and setter functions - not their invocation (I'm not suggesting I should be able to say obj.getter.call(this) or similar).

Despite the actual to-ing & fro-ing in the related tickets about whether this is possible, "valid" or desirable, this is perfectly valid JS use case, and mixins etc use it quite commonly, and being able to accurately model them in Typescript would be as advantageous as modelling any of the other JS paradigms.

Anyone in TS dev able to point me in the right direction if I want to try implementing this?

@magicdawn
Copy link

I think this is WRONG fixed for #36883

from #36883

interface Unimplemented {
    calculate(): number;
}

class Demo {
   get a(this: Unimplemented) {
        return this.calculate();
    }
    b(this: Unimplemented) {
        return this.calculate();
    }
}
const x = new Demo();
console.log(x.a); // no type error, fails at runtime

since this code is cheating/instrucmenting TS compiler the this type, no type error, fails at runtime is expected and should be allowed. If u want a type error, just stop cheating.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants