The Wayback Machine - https://web.archive.org/web/20201112030501/https://github.com/microsoft/TypeScript/issues/2521
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

Suggestion: allow get/set accessors to be of different types #2521

Closed
irakliy81 opened this issue Mar 26, 2015 · 122 comments
Closed

Suggestion: allow get/set accessors to be of different types #2521

irakliy81 opened this issue Mar 26, 2015 · 122 comments

Comments

@irakliy81
Copy link

@irakliy81 irakliy81 commented Mar 26, 2015

It would be great if there was a way to relax the current constraint of requiring get/set accessors to have the same type. this would be helpful in a situation like this:

class MyClass {

    private _myDate: moment.Moment;

    get myDate(): moment.Moment {
        return this._myDate;
    }

    set myDate(value: Date | moment.Moment) {
        this._myDate = moment(value);
    }
}

Currently, this does not seems to be possible, and I have to resort to something like this:

class MyClass {

    private _myDate: moment.Moment;

    get myDate(): moment.Moment {
        return this._myDate;
    }

    set myDate(value: moment.Moment) {
        assert.fail('Setter for myDate is not available. Please use: setMyDate() instead');
    }

    setMyDate(value: Date | moment.Moment) {
        this._myDate = moment(value);
    }
}

This is far from ideal, and the code would be much cleaner if different types would be allowed.

Thanks!

@danquirk
Copy link
Member

@danquirk danquirk commented Mar 26, 2015

I can see how this would be nice here (and this has been requested before although I can't find the issue now) but it's not possible whether or not the utility is enough to warrant it. The problem is that accessors are not surfaced in .d.ts any differently than normal properties since they appear the same from that perspective. Meaning there's no differentiation between the getter and setter so there's no way to a) require an implementation use an accessor rather than a single instance member and b) specify the difference in types between the getter and setter.

@danquirk danquirk closed this Mar 26, 2015
@danquirk danquirk added the By Design label Mar 26, 2015
@irakliy81
Copy link
Author

@irakliy81 irakliy81 commented Mar 27, 2015

Thank you for the quick reply, Dan. I'll follow the less elegant way. Thanks for the great work!

@tkrotoff
Copy link

@tkrotoff tkrotoff commented Jun 11, 2015

A JavaScript getter and setter with different types is perfectly valid and works great and I believe it is this feature's main advantage/purpose. Having to provide a setMyDate() just to please TypeScript ruins it.

Think also about pure JS libraries that will follow this pattern: the .d.ts will have to expose an union or any.

The problem is that accessors are not surfaced in .d.ts any differently than normal properties

Then this limitation should be fixed and this issue should stay open:

// MyClass.d.ts

// Instead of generating:
declare class MyClass {
  myDate: moment.Moment;
}

// Should generate:
declare class MyClass {
  get myDate(): moment.Moment;
  set myDate(value: Date | moment.Moment);
}

// Or shorter syntax:
declare class MyClass {
  myDate: (get: moment.Moment, set: Date | moment.Moment);
  // and 'fooBar: string' being a shorthand for 'fooBar: (get: string, set: string)'
}
@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Jun 11, 2015

I realize this is just an opinion, but writing a setter such that a.x === y is not true immediately after a.x = y; is a giant red flag. How do consumers of a library like that know which properties have magic side effects and which don't?

@tkrotoff
Copy link

@tkrotoff tkrotoff commented Jun 11, 2015

How do [you] know which properties have magic side effects and which don't?

In JavaScript it can be un-intuitive, in TypeScript the tools (IDE + compiler) will complain. Why do we love TypeScript again? :)

@kitsonk
Copy link
Contributor

@kitsonk kitsonk commented Jun 11, 2015

A JavaScript getter and setter with different types is perfectly valid and works great and I believe it is this feature's main advantage/purpose.

This is arguing that JavaScript is weakly typed, so TypeScript should be weakly typed. :-S

@tkrotoff
Copy link

@tkrotoff tkrotoff commented Jun 11, 2015

This is arguing that JavaScript is weakly typed, so TypeScript should be weakly typed

C# allows it and that does not make this language weakly typed C# does not allow get/set of different types.

@kitsonk
Copy link
Contributor

@kitsonk kitsonk commented Jun 12, 2015

C# allows it and that does not make this language weakly typed C# does not allow get/set of different types.

😉

@paulwalker
Copy link

@paulwalker paulwalker commented Oct 21, 2015

No one is arguing (as far as I can see) that accessors should be weakly typed, they are arguing that we should have the flexibility to define the type(s).

Often it is needed to copy a plain old object to object instances.

    get fields(): Field[] {
      return this._fields;
    }

    set fields(value: any[]) {
      this._fields = value.map(Field.fromJson);
    }

That is much nicer than the alternative and allows my setter to encapsulate the exact type of logic setters are made for.

@danquirk
Copy link
Member

@danquirk danquirk commented Oct 21, 2015

@paulwalker the pattern you use there (a setter taking an any and a getter returning a more specific type) is valid today.

@paulwalker
Copy link

@paulwalker paulwalker commented Oct 21, 2015

@danquirk Great to know, thank you! It looks like I just needed to update my IDE plugin compiler for ST.

@jasongrout
Copy link

@jasongrout jasongrout commented Nov 2, 2015

I just tested with typescript@next (Version 1.8.0-dev.20151102), and also have an error.

~$ tsc --version
message TS6029: Version 1.8.0-dev.20151102
~$ cat a.ts
class A {
    get something(): number {return 5;}
    set something(x: any) {}
}

~$ tsc -t es5 a.ts
a.ts(2,2): error TS2380: 'get' and 'set' accessor must have the same type.
a.ts(3,2): error TS2380: 'get' and 'set' accessor must have the same type.
@paulwalker
Copy link

@paulwalker paulwalker commented Nov 2, 2015

Ironically, after updating my Sublime linter, it no longer threw an error, which is using TypeScript 1.7.x. I've been assuming it is a forthcoming enhancement in 1.7+, so perhaps 1.8.0 regressed.

@christianacca
Copy link

@christianacca christianacca commented Dec 19, 2015

Even with the version of visual studio code (0.10.5 (December 2015)) that supports typescript 1.7.5 and with typescript 1.7.5 installed globally on my machine this is still a problem:

image

So what version this will be supported?
Thanks

@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Dec 19, 2015

I think Dan was mistaken. The getter and the setter must be of identical type.

@christianacca
Copy link

@christianacca christianacca commented Dec 19, 2015

shame. would have been a nice feature for when writing page objects for use in protractor tests.

I would have been able to write a protractor test:

po.email = "[email protected]";
expect(po.email).toBe("[email protected]");

... by authoring a page object:

class PageObject {
    get email(): webdriver.promise.Promise<string> {
        return element(by.model("ctrl.user.email")).getAttribute("value")
    }
    set email(value: any) {
        element(by.model("ctrl.user.email")).clear().sendKeys(value);
    }
}
@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Dec 19, 2015

What about that code requires the setter to be of type any ?

@christianacca
Copy link

@christianacca christianacca commented Dec 19, 2015

The getter will return a webdriver.promise.Promise<string> yet the setter I want to assign a string value.

Maybe the following longer form of the protractor test makes it clearer:

po.email = "[email protected]";
var currentEmail : webdriver.promise.Promise<string> = po.email;
expect(currentEmail).toBe("[email protected]")
@Arnavion
Copy link
Contributor

@Arnavion Arnavion commented Mar 22, 2016

@RyanCavanaugh With the introduction of null annotations, this prevents code that allows calling a setter with null to set it to some default value.

class Style {
    private _width: number = 5;

    // `: number | null` encumbers callers with unnecessary `!`
    get width(): number {
        return this._width;
    }

    // `: number` prevents callers from passing in null
    set width(newWidth: number | null) {
        if (newWidth === null) {
            this._width = 5;
        }
        else {
            this._width = newWidth;
        }
    }
}

Could you consider atleast allowing the types to differ in the presence of | null and | undefined ?

@MaklaCof
Copy link

@MaklaCof MaklaCof commented Aug 9, 2016

This would really be a nice feature.

@artyil
Copy link

@artyil artyil commented Sep 18, 2016

So will this be a feature?

@kitsonk
Copy link
Contributor

@kitsonk kitsonk commented Sep 19, 2016

@artyil it is closed and tagged By Design which indicates that currently there is not any plans to add it. If you have a compelling use case which you think overrides the concerns expressed above, you can feel free to make your case and additional feedback may make the core team reconsider their position.

@paulwalker
Copy link

@paulwalker paulwalker commented Sep 19, 2016

@kitsonk I think more than enough compelling use cases have been provided in the above comments. While the current design follows a common pattern of most other typed languages with this restriction, it is unnecessary and overly restrictive in the context of Javascript. While it is by design, the design is wrong.

@MaklaCof
Copy link

@MaklaCof MaklaCof commented Sep 19, 2016

I agree.

@mhegazy mhegazy reopened this Sep 19, 2016
@mhegazy mhegazy removed the By Design label Sep 19, 2016
@EamonNerbonne
Copy link

@EamonNerbonne EamonNerbonne commented May 19, 2020

@MikeyBurkman

[...] but in a vast majority of cases where you'd use TS (including the examples in this thread), I would argue that needing operator overloading is probably a code smell [...]

 foo.x = y; 
 if (foo.x === y) { // this could be false?

So, merely having the types match does not prevent surprising behavior, and indeed el.style.display = ''; //so is this now true: el.style.display === ''? shows that that's not theoretical either. Even with plain old fields, the assumption doesn't hold for NaN, by the way.

But more importantly, your argument ignores the point that TS can't really be opinionated about these things because TS needs to interop with existing JS apis, including major and unlikely to change things like the DOM. And as such, it just doesn't matter whether or not the api is ideal; what matters is that TS can't interop cleanly with such given apis. Now, you're forced to either re-implement whatever fallback logic the api used internally to coerce an out-of-getter-type value passed to the setter and thus type the property as the getters type, or to ignore type checking to add useless type assertions at each getters site and thus type the property as the setters type. Or, worse: do whatever TS happens to choose to annotate for the DOM, regardless of whether that is ideal.

All those choices are bad. The only way to avoid that is to accept the need to represent JS apis as faithfully as possible, and here: that seems possible (admittedly without any knowledge of TS internals that may make this decidedly non-trivial).

@gmurray81
Copy link

@gmurray81 gmurray81 commented May 19, 2020

that seems possible (admittedly without any knowledge of TS internals that may make this decidedly non-trivial)

I suspect some of the new type declaration syntax they enabled more recently could make this expressible when it was less feasible before, so they should really consider reopening this issue... @RyanCavanaugh

@MikeyBurkman
Copy link

@MikeyBurkman MikeyBurkman commented May 19, 2020

In an ideal world TS would interop with all JS APIs. But that's not the case. There are a bunch of JS idioms that can't be typed correctly in TS. However, I'd rather them focus on fixing things that actually lead to better programming idioms, not worse. While supporting this in a sane way would be nice, there are a dozen other things I'd personally rather them spend their limited time on first. This is way down on that list.

@calebjclark
Copy link

@calebjclark calebjclark commented May 19, 2020

@MikeyBurkman the question of priority is valid. My primary concern is @RyanCavanaugh's decision to Close this issue and write it off. Dismissing all the issues pointed out on this thread directly conflicts with Typescript's stated mission, which is to be a "superset" of Javascript (instead of a subset).

@MikeyBurkman
Copy link

@MikeyBurkman MikeyBurkman commented May 19, 2020

Yeah I can definitely agree that this issue probably shouldn't be closed, as it's something that probably should be eventually fixed. (Though I really doubt it will be.)

@gmurray81
Copy link

@gmurray81 gmurray81 commented May 19, 2020

While supporting this in a sane way would be nice, there are a dozen other things I'd personally rather them spend their limited time on first

I think you are underselling an important aspect of TypeScript in that it's supposed to make it safer to use the existing DOM APIs and existing JS APIs. It exists to keep more than just your own code, within it's bubble, safe.

Here's my point of view, I build component libraries, and while I wouldn't necessarily deliberately cause this mismatch between setters/getters to exist given my druthers. Sometimes my hand is forced based on how my libraries need to interface with other systems in-place. E.g. Angular sets all input properties on a component coming from markup as strings, rather than to do any type coercion based on their knowledge of the target type (at least, last I checked). So, do you refuse to accept strings? Do you make everything a string, even though this would make your types awful to use? Or do you do what TypeScript would have you do and use a type such as: string | Color but make using the getters awful. These are all pretty terrible options that reduce safety, when some extra expressiveness from the type system would have helped.

The problem is, it's not just Angular that causes these issues. Angular wound up in that situation because it mirrors a lot of scenarios in the DOM where auto-coercion happens on a property set, but the getter is always an anticipated singular type.

@thw0rted
Copy link

@thw0rted thw0rted commented May 19, 2020

Look a bit upthread: Angular is much worse than you think.

@fan-tom
Copy link

@fan-tom fan-tom commented Aug 3, 2020

For me it is usually the issue when you want to create wrapper around some general-purpose storage, that can store key-value pairs and you want to limit users to certain keys.

class Store {
  private dict: Map<string, any>;

  get name(): string | null {
    return this.dict.get('name') as string | null;
  }

  set name(value: string) {
    this.dict.set('name', value);
  }
}

You want to have such constraint: user can get null, if value was not previously set, but cannot set it to null. Currently, you cannot do it, because setter input type must include null.

@calebjclark
Copy link

@calebjclark calebjclark commented Aug 3, 2020

@fan-tom, great use-case for why this issue needs to be reopened! Keep 'em coming.

@trusktr
Copy link

@trusktr trusktr commented Aug 4, 2020

This issue has an extremely high number of up votes!

This is TS team's project, so they can do as they see fit however they enjoy. But if they aim to make this as useful as possible for an external community of users, then I hope TS team can take the community's high number of votes into strong consideration.

The homepage of typescriptlang.org is currently inaccurate when it states:

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.

TypeScript is a typed superset of a subset of JavaScript that compiles to plain JavaScript. 😃

@gmurray81
Copy link

@gmurray81 gmurray81 commented Aug 4, 2020

TypeScript is a typed superset of a subset of JavaScript that compiles to plain JavaScript.

More diplomatically, I think you could say it's an opinionated superset of JavaScript, when the team makes this kind of opinionated omission from the type system modelling.

They are saying, "we won't model this, because it's hard for us to do, and we think you shouldn't do it, anyhow". But JavaScript is chock full of things you shouldn't do, and generally Typescript won't block you from doing these things if you have the will and know-how, because it seems like the general strategy is to not be opinionated about what JavaScript you can and can't just run as Typescript.

That's why it's so strange to refuse to model this common (used in the DOM!) scenario, citing the belief that APIs shouldn't perform setter based coercion as the rationale to not do so.

@ui-ankush
Copy link

@ui-ankush ui-ankush commented Aug 10, 2020

Currently, this does not seems to be possible, and I have to resort to something like this:

class MyClass {

    private _myDate: moment.Moment;

    get myDate(): moment.Moment {
        return this._myDate;
    }

    set myDate(value: moment.Moment) {
        assert.fail('Setter for myDate is not available. Please use: setMyDate() instead');
    }

    setMyDate(value: Date | moment.Moment) {
        this._myDate = moment(value);
    }
}

The best you can do is add a constructor and call the custom setter function there, if thats the only time you are setting that value.

constructor(value: Date | moment.Moment) {
    this.setMyDate(value);
}

The problem when assigning values directly still remains.

@xhliu
Copy link

@xhliu xhliu commented Oct 12, 2020

Any update on this, after over 5.5 years?

@kitsonk
Copy link
Contributor

@kitsonk kitsonk commented Oct 13, 2020

@xhliu as the labels indicate, it is a design limitation that is too complex to address, and therefore the issue is closed. I wouldn't not expect an update. The length of time for a closed issue is sort of irrelevant.

@AlexGalays
Copy link

@AlexGalays AlexGalays commented Oct 18, 2020

Please reopen. Also this needs to be added at the interface level. A classic example is when implementing a Proxy where you have full flexibility over what you can read and write.

@SephReed
Copy link

@SephReed SephReed commented Oct 28, 2020

@xhliu ...too complex to address...

https://ts-ast-viewer.com/#code/MYewdgzgLgBFCm0DyAjAVjAvDA3gKBkJgDMQQAuGAIhQEMAnKvAXzzwWXQDpSQg

const testObj = {
    foo: "bar"
}

testObj.foo

The symbol foo has this to say:

Symbol
  foo
    flags: 4
    escapedName:"foo"
    declarations: [
      PropertyAssignment (foo)
    ]
    valueDeclaration: PropertyAssignment (foo)

There's a lot of room here for additional information, which is great design on TS behalf. Whoever designed this probably did so specifically to make additional features (even big ones) possible in the future.

Speculative from this point on:

If it was possible to declare (for pseudo code example) a accessDelclaration: AccessSpecification (foo), then the PropertyAccessExpression which is aware of the symbol foo and its declarations, could conditionally check if there is a "accessDelclaration" and use the typing from that instead.

Assuming the syntax exists to add that accessDelclaration property to the "foo" symbol, the PropertyAccessExpression should be able to pull the "AccessSpecification" from the symbol it gets from ts.createIdentifier("foo") and yield different types.

ts.createPropertyAccess(
  ts.createIdentifier("testObj"),
  ts.createIdentifier("foo")
)

Speculatively, it appears the hardest part of this challenge would likely be the amount of bike-shedding around syntax (or perhaps a company philosophy?), but from an implementation perspective the tools should all be there. A single condition would be added to the ts.createPropertyAccess() function, and a Declaration class to represent that condition and its effects must be added to the symbol of the object property.

@mpawelski
Copy link

@mpawelski mpawelski commented Nov 4, 2020

A lot of good examples have been written why this is desperately needed (especially for DOM and Angular).

I'll just add that I got hit by this today when migrating old JS code to TS where assigning string to window.location didn't worked and I had to do as any workaround 😟

The Window.location read-only property returns a Location object with information about the current location of the document.

Though Window.location is a read-only Location object, you can also assign a DOMString to it. This means that you can work with location as if it were a string in most cases: location = 'http://www.example.com' is a synonym of location.href = 'http://www.example.com'.
source

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Design Meeting Docket
  
Awaiting triage
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.