้•œๅƒ็ซ™็‚น ยท ๆœฌ้กต็”ฑ็ฌฌไธ‰ๆ–น GitHub ๅช่ฏป้•œๅƒๆไพ›๏ผŒ้ž GitHub ๅฎ˜ๆ–น็ซ™็‚น๏ผŒไธๆŽฅๅ—ไปปไฝ•็™ปๅฝ•ๆˆ–ๅ‡ญๆฎ่พ“ๅ…ฅใ€‚ๅ‰ๅพ€ github.com
Skip to content

this index with unions outputs intersections?ย #49393

Description

@u128393

Bug Report

๐Ÿ”Ž Search Terms

  • this index

๐Ÿ•— Version & Regression Information

This changed between versions 3.3.3 and 3.5.1, and it keeps the behavior since then to the current latest version (at least 4.7.3).

โฏ Playground Link

Playground link with relevant code

๐Ÿ’ป Code

class Test {
  a = '';
  b = 0;

  test() {
    const a: this['a'] = '';        // ok
    const b: this['b'] = 0;         // ok
    const c: Test['a' | 'b'] = '';  // ok
    const d: this['a' | 'b'] = '';  // fail
  }
}

๐Ÿ™ Actual behavior

Type 'string' is not assignable to type 'this["a"] & this["b"]'.
  Type 'string' is not assignable to type 'this["b"]'.
    Type 'string' is not assignable to type 'number'.(2322)

๐Ÿ™‚ Expected behavior

I expect this['a' | 'b'] to be string | number in this example, thus accepting the assignment of '' to it, just like Test['a' | 'b'].

Activity

  1. fatcerberus commented on Jun 5, 2022

    @fatcerberus

    This is by design and has nothing to do with this. The reason you get intersections is:

    interface I { a: string, b: number };
    const v: I = { a: "foo", b: 812 };
    const k = Math.random() >= 0.5 ? 'a': 'b';
    
    // note: the type being assigned to below is I['a' | 'b']
    v[k] = "bar";  // error - only 50/50 chance this is type-correct

    The only way the above could be typesafe is you could somehow come up with a value of type string & number.

    See #30769.

  2. jcalz commented on Jun 5, 2022

    @jcalz
    Contributor

    It has something to do with this, otherwise Test['a' | 'b'] and this['a' | 'b'] would behave the same. I suspect the difference is that Test is a specific type, so Test['a' | 'b'] is resolved eagerly to string | number before the compiler is even aware of an assignment. On the other hand, this is implemented as an implicit generic type parameter, and so this['a' | 'b'] stays as an indexed access type, and when you try to assign to such a type you get the intersection restriction as described in #30769.

  3. fatcerberus commented on Jun 5, 2022

    @fatcerberus

    Ah, I didn't realize the case with a concrete type is different. I should know by now that "TypeScript" and "consistency" are two words that don't belong in the same sentence. ๐Ÿ˜†

  4. u128393 commented on Jun 6, 2022

    @u128393
    Author

    I see. It makes sense to me now. But the inconsistency is confusing.

    The original issue I met is actually a little bit different. It may or may not be the same issue as this one I posted.

    class Base {
      numFnWithThis<T extends NumKeys<this>>(key: T) {}
    }
    
    class Sub extends Base {
      a!: number;
      b!: string;
    
      numFnWithConcrete<T extends NumKeys<Sub>>(key: T) {}
    
      test() {
        this.numFnWithThis('a');      // error: Argument of type 'string' is not assignable to parameter of type 'NumKeys<this>'.(2345)
        this.numFnWithConcrete('a');  // ok
      }
    }
    
    type NumKeys<T extends object> = {
      [K in keyof T]: T[K] extends number ? K : never;
    }[keyof T];

    Playground link

    I have a method in base class that accepts certain types of property keys of the concrete class. The type filter helper (NumKeys<T>) works well with a concrete type, but doesn't work with this type.

  5. RyanCavanaugh commented on Jun 6, 2022

    @RyanCavanaugh
    Member

    It's consistent, just consistent with a different set of rules than you were expecting ๐Ÿ™ƒ

    The general way to think of it is that expressions involving this are always deferred within the class, to prevent unsoundnesses from occurring when a derived class has a more-specific type of some property than its base class.

    The second example is tricky because TS is warning you about a condition that, by construction, can't actually occur in this sample. It's not safe in general to eagerly resolve NumKeys to some union in this code, because we don't "know" that NumKeys<some speculative derived type> is a supertype of NumKeys<Sub>. However, the combination of NumKeys being effectively contravariant with the fact that any derived class of Sub has to be a subtype means that this example should be legal, we just don't have the mechanics in place to detect this combination of facts and identify it as actually-OK.

  6. typescript-bot commented on Jun 8, 2022

    @typescript-bot
    Contributor

    This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  7. 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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions