-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Optional chaining with non null operator is unsafe, because it could throw an exception #36031
Comments
Is the precedence of |
@JacksonKearl It is the explicit goal of TypeScript that the syntax should have no effect on the runtime behavior so you can’t just add a bracket out of nowhere. Consider the following case: function getElementById(id: string): Element | null
interface Element {
// Only null for Document, DOCTYPE, or a Notation
textContent: string | null
}
getElementById("div.head")?.textContent!.toUpperCase() |
Ah my bad, yes I see what you mean. In this playground, adding the |
I'll look into it. While the syntax is valid to parse, it is a bit nonsensical. You're asserting that |
I disagree. I think I'm asserting that if // Input:
a?.b!.c
// Output:
a == null ? undefined : a.b!.c |
@rbuckton The code should run the same with or without TS-specific syntax, right? Currently it won't; the output changes depending on the presence of the This further means that the code with run differently when targeting ESNext ( |
As we were implementing the change, it seems that this had some undesirable properties in the type system when you change precedence. Specifically, @rbuckton's approach seemed to mean a?.b.c! would potentially come out as We also checked this behavior out in C# which also has a #nullable enable
using System;
public class C {
public Blah M(Blah? x) {
var a = x?.X!.X;
return a;
}
}
public class Blah
{
public Blah X = null!;
} |
This turns out to be a much more complex and subtle issue than it seems on the surface. Currently, we parse postfix- Generally, the use case for postfix- declare const a: number | undefined;
const x = a!; // number Here, the user expects the type of declare const a: { b: number | null };
const x = a.b!; // number Once again, the user expects the type of When optional chaining is added into the mix, we currently preserve these expectations: declare const a: { b: number | null };
const x1 = a?.b // number | null | undefined* (* undefined added by `?.`)
const x2 = a?.b! // number Here, we pick the type If we change the precedence of As a result, we need to choose one of the following different approaches:
As a result, it is unlikely this will make our 3.8 release milestone as we need to discuss this further within the team. Please note that C# also implements both |
This option is inconsistent with the general TS design goal (as I understand it) that the code should always run the same with or without TypeScript-specific add-ons. I realize that's not explicitly stated in the design goals, but are there any other examples of TS that behaves differently if you remove the TS-specific add-ons? |
I don't think that's the right way to think about it because ultimately it's not really the same code, it's a distinct syntax that was parsed in a meaningfully different way. The way it's parsed typically implies a certain order of operations. If you want to desugar the current code and build a mental model around it, (a?.b as NonNullable<typeof a>["b"] | undefined).c which is downeleveled to (a?.b).c And that code does shorten the optional chain expression's reach due to the parenthesized expression. So the question isn't about erasability because erasure is happening either way. It's about parsing precedence. |
I think this is where we disagree. I expect If I wanted to unconditionally assert the return type, I would have written Downleveling |
Exactly, and given there's no way to represent what |
That's all well and good if you have the TS shift-reduce conflict resolution table memorized, but if you're a random developer who encounters an error like this: Realizes that they know And this isn't just some hypothetical example, VSCode 1.42 will ship tomorrow with an instance of this exact bug: https://github.com/microsoft/vscode/blob/master/src/vs/base/browser/ui/tree/asyncDataTree.ts#L272 How would you propose people address the declare const foo: { bar?: { baz?: () => {} } }
foo.bar?.baz(); given they know This is my best shot: declare const foo: { bar?: { baz?: () => {} } }
if (foo.bar) {
(foo.bar.baz as Exclude<Exclude<typeof foo.bar, undefined>['baz'], undefined>)()
} ...which is to say, get rid of the optional chaining entirely. |
Yes, that's understandable, but then the argument you're giving is developer experience, it's not that TypeScript isn't providing erasable syntax on top of JS. Maybe it seems like I'm not empathizing with the problem you're running into (I am!). I'm just trying to clarify why this isn't inconsistent with our design goals and I'd like the arguments presented here to be accurate. I think next steps will be to
|
There is, but it's
I'd suggest it's both: It's a poor developer experience because TS isn't providing erasable syntax. |
Ah, I knew there must be something but I blanked on what it was.
Exactly. It's breaking the contract TS has set up with users that generally goes: "if I add TS syntax elements to my JS the JS will continue to run the exact same". Are there any other examples in all of TS where adding some TS-specific syntax effects the emit? The only one I can think of is generators, and those at least are very explicit about being runtime features. From TS docs:
|
The "erasable syntax" case would be handled by forcing parens (e.g., |
@rbuckton I think that's the best option if changing the precedence is too high-impact. |
After our most recent design meeting, we've decided that our solution should be a slightly odd hybrid to satisfy both the
|
Love it. |
A bug of CFA #36958 should be fixed at the same time or the patch should be possible easily to be fixed after. const m = ''.match('');
m?.[0] && m[0]; // ok
m?.[0]! && m[0]; // error
m?.[0].length! > 0 && m[0]; // error
m?.[0].split('').slice() && m[0]; // ok
m?.[0].split('')!.slice() && m[0]; // error |
TypeScript Version: 3.7.2
Search Terms: optional chaining, non null operator
Code
This input
will compile to
But it's unsafe, because if
a
isnull
orundefined
, will throw an exception:IMO we should not raise this exception.
Expected behavior:
Would be better to compile to
So
a
could be null or undefined and it'll not raise an exception anymore.Playground Link: Playground
Related Issues: There was a lot of discussion here, but was more focused in the type system, not about the code output that raise wrongly an exception.
!.
after?.
should be warned #35071Also this bug was reported in Babel and already there is a PR to fix that in Babel:
In this issue I'm saying only about the output, not about the type system.
The text was updated successfully, but these errors were encountered: