-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Different behavior for a?.b<c>.d
depending on the compilation target
#49293
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
Comments
@jakebailey good luck π |
The parser parses this the same as Comparing this to See: Playground Link My gut is telling me that this is just a downleveling bug and we just aren't skipping past the type arguments somewhere (since the type args won't be emitted). |
That being said, I think there's also a checker bug here that's related. In writing a test case, I get a confusing declare namespace A {
export class b<T> {
static d: number;
constructor(x: T);
}
}
type c = unknown;
declare const a: typeof A | undefined;
a?.b<c>.d; // Error
a?.b.d // No error |
Confusingly, I don't always get this error in my test, but I see it in the editor and on the playground. |
I think there's some problematic AST API surface that was missed when we added instantiation expressions; specifically, we have the notion of "access chain" branded types ( Because we parse @ahejlsberg @rbuckton may have thoughts here. |
Hm, scratch that; given it's a I'll keep digging and stop pinging people until I have a real view. |
It seems to be a mix of everything I listed above; for example export function isExpressionOfOptionalChainRoot(node: Node): node is Expression & { parent: OptionalChainRoot } {
return isOptionalChainRoot(node.parent) && node.parent.expression === node;
} Which use a type guard to say that a parent is a chain root, when for example, Then in |
We talked about this in today's design meeting. What we're going to do is to disallow a The rationale here is that if I write code that looks like this: a.b<c>.d That because I haven't actually instantiated anything yet (new'd the thing I instantiated, called, whatever), any property of declare namespace A {
export class b<T> {
static d: number;
constructor(x: T);
}
}
type c = 'c type';
declare const a: typeof A | undefined;
a?.b<c>.d;
|
That sounds reasonable! |
Q: If you will disallow |
"Disallow" is probably too strong of a term; what I mean is that we'll bail out of parsing the In your example, I believe the |
Actually, replying to myself: what would be the difference between (sorry, I'm just dumping my thoughts here in case they are interesting for you π ) |
This comment was marked as outdated.
This comment was marked as outdated.
I had a silly typo: declare interface A {
c: number;
<T>(): T;
}
type b = 'b type';
declare const a: A | undefined;
a?.<b>();
a<b>?.(); We emit:
Which is... weird, but functionally equivalent (same as if you do |
TheΒ secondΒ one isΒ correct, becauseΒ ifΒ |
That sounds unrelated; the top one is how we emit EDIT: I checked, and that has been the behavior since TS v3.8. In v3.7, TS did emit |
In practice it's rare to trigger that behavior. We do guard against situations like these with [1] Ugh with the exception of |
@DanielRosenwasser In |
Hey, I realized that there is at least one case where a property access after an instantiation expression is meaningful: function f<T extends 0 | 1 | 2 | 3>(this: T, a: T) {
return this + a;
}
let fn1 = f<0 | 1>.bind(1);
let fn2 = f<0 | 1 | 2>.bind(1);
fn1(2); // error
fn2(2); // ok |
That's an interesting case. Another example would be something like We talked about this in the design meeting and while it feels like the true problem is the optional chaining, we're not certain enough about this syntax to allow it. We'd rather be conservative here about the syntax, given this is a hole we didn't see when the feature was merged. We can always revisit and allow this syntax, or some forms, but the main goal right now is to make sure people aren't writing spuriously incorrect code like the one in the original issue without knowing. A workaround is to instantiate this and save it in a variable: const fn = f<0 | 1>;
fn.bind(...) |
Bug Report
π Search Terms
instantiation expression optional chaining target
π Version & Regression Information
This feature has been introduced in 4.7. I tested 4.7.0-dev.20220528
β― Playground Link
Playground link with relevant code
π» Code
π Actual behavior
When targeting <=ES2019 it generates
(a === null || a === void 0 ? void 0 : a.b).d
, while when targeting >=ES2020 it generatesa?.b.d
.They have different behaviors: in the ES2019 output it always reads
.d
, while in the ES2020 output it only reads.d
ifa
is not nullish.π Expected behavior
They should have the same behavior.
The ES2020 behavior feels more useful, but "adding
<c>
to an optional chain ends the optional context" (as wrapping it in parentheses does) would be ok too as long as it's consistent. You would then have to writea?.b<c>?.d
.(Babel's implementation has the same different behavior depending on the target, so this is apparently something very easy to overlook while implementing instantiation expressions π¬)
The text was updated successfully, but these errors were encountered: