Skip to content

setter inferred from union has incorrect variance #56894

Closed

Description

🔎 Search Terms

setter union variance

is:issue in-title: setter (nothing relevant found)

is:issue in-title: setter union variance (nothing relevant found)

🕗 Version & Regression Information

confirmed 4.3.5 ~ 5.3.2

⏯ Playground Link

playground

💻 Code

declare const id:<T>()=>T

function f2(x: {f:((a:1|2)=>1|2)} | {f:((a:2|3)=>2|3)} ){
    const r = x.f(id())
//                ^ const id: <2>() => 2,
//        ^ const r: 1 | 2 | 3
}

function f3(f: ((a:1|2)=>1|2) | ((a:2|3)=>2|3) ){
    const r = f(id())
//              ^ const id: <2>() => 2,  
//        ^ const r: 1 | 2 | 3
}


function f4(x: {set d(a:1|2); get r():1|2} | {set d(a:2|3); get r():2|3} ){
//           ^ (parameter) x: { d: 1 | 2; readonly r: 1 | 2; } | { d: 2 | 3; readonly r: 2 | 3; }
    x.d = id();
//        ^ const id: <1 | 2 | 3>() => 1 | 2 | 3
    x.r; 
//    ^    (property) r: 1 | 2 | 3
}

🙁 Actual behavior

function f4(x: {set d(a:1|2); get r():1|2} | {set d(a:2|3); get r():2|3} ){
    x.d = id();
//        ^ const id: <1 | 2 | 3>() => 1 | 2 | 3

The setter function has range 1|2|3.

🙂 Expected behavior

function f4(x: {set d(a:1|2); get r():1|2} | {set d(a:2|3); get r():2|3} ){
//           ^ (parameter) x: {set d(a:1|2); get r():1|2;} | {set d(a:2|3); get r():2|3;}
    x.d = id();
//        ^ const id: <2>() => 2
  • The setter function has range 2, because that is the valid range that can be assigned to both setter functions in the union.
    • Compare to the ranges 2 and 2 computed for function parameters a in f2 and f3.
  • The type display for x should show getters and setters instead of plain (readonly for getter) properties.

Additional information about the issue

  • The parameter range of the union of two functions is the intersection of those two functions parameter ranges. That is because anything outside of that intersection would be invalid input in at least one of the two.

  • Theoretically, writes and reads of plain properties of unions of object could also "legally" have opposite variances, with writes being narrower - but they are not treated that way, which although unsound is ok because type widening is allowed for convenience.

  • For the special cases where user want a setter, or setter-getter same-name pair, to behave write-unsoundly exactly like a plain property, they can specify the type as being a plain type, e.g. { d: T } instead of { set d(value:T): }. That's an argument for allowing write-sound behavior by default in a type definition { set d(value:T): }.

  • The setters are converted to plain properties (and the getters are converted to a readonly plain properties (probably at point of declaration) as can be seen by tool tipping to x which shows (parameter) x: { d: 1 | 2; readonly r: 1 | 2; } | { d: 2 | 3; readonly r: 2 | 3; }. Related to Flow is improperly narrowing getter values #56899.

Activity

  1. Andarist commented on Dec 29, 2023

    @Andarist
    Contributor

    The issue title should mention setters here :p I prepared a fix for this one: #56895

    I also have found that variance is actually computed incorrectly (or rather... it's not observable at all):

    // this really should be `in` but you can make it whatever you want here, TS won't complain
    type MyType<out T> = {
      set k(a: T);
      get k(): unknown;
    };

    The problem is that isPropertySymbolTypeRelated doesn't account for write types at all. Perhaps this deserves a new issue?

  2. fatcerberus commented on Dec 29, 2023

    @fatcerberus

    Theoretically, writes and reads of plain properties of unions of object could also "legally" have opposite variances

    They actually do, in certain cases. See e.g. #30769.

  3. changed the title [-]getter inferred from union has incorrect variance[/-] [+]setter inferred from union has incorrect variance[/+] on Dec 29, 2023
  4. craigphicks commented on Dec 29, 2023

    @craigphicks
    Author

    Mateusz Burzyński (@Andarist)

    The issue title should mention setters here :p I prepared a fix for this one: #56895

    I also have found that variance is actually computed incorrectly (or rather... it's not observable at all):

    // this really should be `in` but you can make it whatever you want here, TS won't complain
    type MyType<out T> = {
      set k(a: T);
      get k(): unknown;
    };

    The problem is that isPropertySymbolTypeRelated doesn't account for write types at all. Perhaps this deserves a new issue?

    Please help me. I don't understand why in or out are associated with types rather than property names, how out differs from readonly, and how in differs from set <property name>(value): void.

  5. craigphicks commented on Dec 29, 2023

    @craigphicks
    Author

    I am re-opening this issue because because it seems to have been incorrectly closed without any resolution attributed.

  6. craigphicks commented on Dec 29, 2023

    @craigphicks
    Author

    Bruce Pascoe (@fatcerberus)

    Theoretically, writes and reads of plain properties of unions of object could also "legally" have opposite variances

    They actually do, in certain cases. See e.g. #30769.

    Great reference. However, I am puzzled by this test case:

    function f3<T extends { [key: string]: any }>(obj: T) {
        let foo = obj['foo'];
        let bar = obj['bar'];
        obj['foo'] = 123;  // Error
        obj['bar'] = 'x';  // Error
    }

    I can't see the reasoning for the errors. This is runtime-error-free node JS:

    > obj = {}
    {}
    > obj.a = 1
    1
    > obj["b"] = 2
    2
    
  7. Andarist commented on Dec 29, 2023

    @Andarist
    Contributor

    It likely prevents invalid writes in situations like this:

    declare const obj: {
      foo: boolean;
      bar: boolean;
    };
    
    f3(obj);
  8. craigphicks commented on Dec 29, 2023

    @craigphicks
    Author

    It likely prevents invalid writes in situations like this:

    declare const obj: {
      foo: boolean;
      bar: boolean;
    };
    
    f3(obj);

    Another function format exists to get that same error behavior:

    function f5<T extends Readonly<{ [key: string]: any }>>(obj: T) {
        let foo = obj['foo'];
        let bar = obj['bar'];
        obj['foo'] = 123;  // Error
        obj['bar'] = 'x';  // Error
    }
    f5(obj);

    What function should the user call when they don't want those errors?
    I think it would be better to trust that the user is reading the function declaration and understands what it means.

  9. craigphicks commented on Jan 2, 2024

    @craigphicks
    Author

    Closing as this is superseded by #56922

  10. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions