TypeScript: Allow setter type to be incompatible with the getter type

Suggestion

🔍 Search Terms

differing accessor types

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn’t be a breaking change in existing TypeScript/JavaScript code
  • This wouldn’t change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn’t a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript’s Design Goals.

⭐ Suggestion

#42425 forces the getter type to be assignable to the setter type. This makes the feature limited on the following DOM typing case, and thus it could be great if the limitation can be lifted.

📃 Motivating Example

[Exposed=Window]
interface CSSStyleRule : CSSRule {
  attribute CSSOMString selectorText;
  [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style;
};
interface CSSStyleRule extends CSSRule {
    selectorText: string;
    get style(): CSSStyleDeclaration;
    set style(cssText: string); // currently an error
}
document.body.style = "display: none" // thus still an error while the actual behavior allows it đŸ€”

đŸ’» Use Cases

Allows Web IDL readonly attributes to be assignable, and thus the DOM typing better matches the actual behavior.

Transferred from https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/996.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 54
  • Comments: 22 (5 by maintainers)

Commits related to this issue

Most upvoted comments

Hi y’all, I’d like +1 this proposal as well. In Relay (relay.dev), we are working on a feature where graphql linked fields can be updated with simple assignment, instead of the ugly existing API recordProxy.setLinkedRecord(...). A simplified example would be:

const updatableData = readUpdatableQuery(graphql`
  query FooQuery @updatable {
    best_friend {
      ...AssignableBestFriend_user
      name
    }
  }
`, store);
// the type of updatableData would be as follows
type UpdatableData = {
  get best_friend(): { name: ?string },
  set best_friend(value: { $fragmentRefs: AssignableBestFriend_user }): void,
}

This allows us to enforce that any user that can be assigned to best_friend must have had the AssignableBestFriend_user fragment spread at that location. Essentially, using this mechanism, we are letting the Relay compiler validate assignments. However, we don’t want to enforce that any field that is assigned to best_friend must also have a name selection, or to include the $fragmentRefs field from the getter.

(This is only part of what is required to ensure that the assignment is valid, but the rest is irrelevant for the getter/setter discussion.)

Thank you! It would be a pity to be able to roll this out with full type safety for users of Flow, but to have limited type safety in Typescript.

I actually brought this up a few weeks ago and we agreed it’s worth some experimenting during 5.1. I just forgot to update the issue afterwards; thanks for the reminder.

To be fair though, #2521 is closed because #42425 was thought to address it. This issue was raised afterwards to present use cases that #42425 did not address.

+1 for this feature. Flagging this issue that contains lots of great use cases for incompatible getter/setter types: https://github.com/microsoft/TypeScript/issues/2521

Lending my support to this proposal. Thankfully language ability trumps subjective opinion. If we wanted to write a bunch of getThing and setThing methods, we’d probably be in Java.

Thank you!

A periodic reminder to the TS staff that the community has been asking for this since 2015 (#2521).

Why not just make a Get
() and Set
() function? Getters and setters are supposed to be so simple that they are similar in use as fields.

Perhaps a unique use case, but I’ll log mine here. I wanted to allow providing an update function to pass to Immer in addition to just setting the new value. This works fine in JS, but I can’t get the Type definitions to comply

console.log(someObj.profile) // { name: 'John Doe' }
someObj.profile = { name: 'This works' }
someObj.profile = (draft) => {
  draft.name = 'This does not'
}

use case:

getter/setter in mapped type for proxy value with different types for reactive (rxjs like) api

I have Proxy object, that simplifies api for rxjs like values
When I get value from this Proxy, I just read last memoized value
When I assign to this value, I should be able to assign both ‘updater’ or value type

Example code:

values: MyProxy<{ x: number, y: number }>;

values.x = 10;
values.y = () => values.x + 10;

const finalY = values.y;

Type of Proxy here should be:

{
    get [K in keyof T](): T[K];
    set [K in keyof T](value: T[K] | (() => T[K]));
}

Not found any hacks/workarounds here, because mapping object with get/set captures only getter’s type:

{
        [k in keyof T]: {
            get x(): T[k];
            set x(val: T[k] | (() => T[k]));
        }["x"];
}

// results in

{
        [k in keyof T]: T[k]
}

Actually typescript should add ‘set’ modifier for fields along with eg ‘readonly’; Which should work same way and check assigment type:

type T;

type T_add_set = {
    set [k in keyof T]: T[k] | (() => T[k])
}

type T_remove_set = {
    -set [k in keyof T_add_set]: T_add_set[k]
}

And ‘get’ modifier is just ‘readonly’ which will be resolved similar to function overloading (if ‘readonly’ and ‘set’ presents for same field)

I feel like the pot holes generated by having different get/set types can also be overcome by TypeScript.

I believe most people are going to support same types and over load them to be different as well for example

get id(): string {}
set id(): string | number {}

string | number extends string, so even JavaScript developers who are expecting get and set to be the same type shouldn’t have any issues

Edit: This argument is invalid. This is the default behavior.

Just wanted to flag another Web API that isn’t able to be used without this feature

Your code is equivalent to this:

const url = new URL("https://example.com");
url.search = new URLSearchParams({ test: "test" }).toString();


 as URLSearchParams has special stringification behavior. So no, it can be used without this feature.

Also, in case it matters, el.style = el.style might not do what you want, but it does not cause a runtime error.

I mean, it will literally never do what you want or expect, unless you just want to watch the world DOM burn. Preventing runtime errors is kind of priority one, but preventing blatantly obvious behavioral errors is also a goal.

Just spent an hour debugging an error caused by a junior dev that never would have existed if TypeScript could recognize getter-less properties as such. Would be great if this issue were prioritized.

@MaximSagan If there is no getter, then the type would be never meaning that it will always extend the setter type. If this issue gets “fixed”, then it will actually hide your problem even further. As @thw0rted commented, you want a linter rule.

Or you can just scream at the junior dev for not making it an idiomatic setProperty() function

Just wanted to flag another Web API that isn’t able to be used without this feature:

The URL.search getter returns string, but the setter also accepts a URLSearchParams object, so the following fails to compile:

const url = new URL("https://example.com");
url.search = new URLSearchParams({ test: "test" });

with error TS2322: Type 'URLSearchParams' is not assignable to type 'string'., despite it being valid.

Also, in case it matters, el.style = el.style might not do what you want, but it does not cause a runtime error.

That should still be a static error because it’s virtually el.style = el.style.toString() where the string becomes [object CSS2Properties].

In the “Restrictions” section near the top of #42425, Ryan said that “obj.x = obj.x; must be legal assuming obj.x is writable at all” in order to “prevent novel unsoundness from occurring”. Smarter people than I will have to tell you exactly why that’s so but it also seems to align with the Goals:

  • Goal: Statically identify constructs that are likely to be errors. – designing an API that does not allow x=x sure seems “likely to be an error”
  • Non-goal: Introduce behaviour that is likely to surprise users – the DOM violated the POLS here, and it should feel bad.

Also, in case it matters, el.style = el.style might not do what you want, but it does not cause a runtime error.