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

this index with unions outputs intersections? #49393

Closed
u128393 opened this issue Jun 5, 2022 · 6 comments
Closed

this index with unions outputs intersections? #49393

u128393 opened this issue Jun 5, 2022 · 6 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@u128393
Copy link

u128393 commented Jun 5, 2022

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

@fatcerberus
Copy link

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.

@jcalz
Copy link
Contributor

jcalz commented Jun 5, 2022

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.

@fatcerberus
Copy link

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. πŸ˜†

@u128393
Copy link
Author

u128393 commented Jun 6, 2022

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.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Jun 6, 2022
@RyanCavanaugh
Copy link
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.

@typescript-bot
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants