interface OptionalMemberExpression <: Expression {
type: "OptionalMemberExpression";
object: Expression;
property: Expression;
computed: boolean;
optional: boolean;
}
interface OptionalCallExpression <: Expression {
type: "OptionalCallExpression";
callee: Expression;
arguments: [ Expression | SpreadElement ];
optional: boolean;
}
The Optional(Call|Member)Expression
aligns to the OptionalChain
production. When optional
is true
, the property/arguments must follow a ?.
token. Any member/call in the optional chain is considered optional call or optional member. That said, if a node is Optional(Call|Member)Expression
, there must exist a Optional(Call|Member)Expression[optional=true]
to its left that starts this optional chain.
computed
property is always false
in the following examples. It can extend to computed properties w.l.o.g.
// obj?.aaa?.bbb
{
"type": "OptionalMemberExpression",
"object": {
"type": "OptionalMemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" },
"optional": true
},
"property": { "type": "Identifier", "name": "bbb" },
"optional": true
}
// obj?.aaa.bbb
{
"type": "OptionalMemberExpression",
"object": {
"type": "OptionalMemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" },
"optional": true
},
"property": { "type": "Identifier", "name": "bbb" },
"optional": false
}
bbb
is a part of optional chain, so it is OptionalMemberExpression
, but it does not follow a ?.
token, so it has optional: false
setting.
// obj.aaa?.bbb
{
"type": "OptionalMemberExpression",
"object": {
"type": "MemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" }
},
"property": { "type": "Identifier", "name": "bbb" },
"optional": true
}
// obj.aaa.bbb
{
"type": "MemberExpression",
"object": {
"type": "MemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" }
},
"property": { "type": "Identifier", "name": "bbb" }
}
This proposal does not introduce changes for member expressions obj.aaa.bbb
.
// (obj?.aaa)?.bbb
{
"type": "OptionalMemberExpression",
"object": {
"type": "OptionalMemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" },
"optional": true
},
"property": { "type": "Identifier", "name": "bbb" },
"optional": true
}
Both (obj?.aaa)?.bbb
and obj?.aaa?.bbb
shares the same AST structures because they are equivalent. Just like (obj.aaa).bbb
is same as obj.aaa.bbb
.
// (obj?.aaa).bbb
{
"type": "MemberExpression",
"object": {
"type": "OptionalMemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" },
"optional": true
},
"property": { "type": "Identifier", "name": "bbb" }
}
However (obj?.aaa).bbb
is not equivalent to obj?.aaa.bbb
. Because .bbb
is not a part of the optional chain, it is now a member expression.
// func?.()?.bbb
{
"type": "OptionalMemberExpression",
"object": {
"type": "OptionalCallExpression",
"callee": { "type": "Identifier", "name": "func" },
"arguments": [],
"optional": true
},
"property": { "type": "Identifier", "name": "bbb" },
"optional": true
}
// func?.().bbb
{
"type": "OptionalMemberExpression",
"object": {
"type": "OptionalCallExpression",
"callee": { "type": "Identifier", "name": "func" },
"arguments": [],
"optional": true
},
"property": { "type": "Identifier", "name": "bbb" },
"optional": false
}
.bbb
is a part of optional chain, so it is OptionalMemberExpression
, but it does not follow a ?.
token, so it has optional: false
setting.
// (func?.())?.bbb
{
"type": "OptionalMemberExpression",
"object": {
"type": "OptionalCallExpression",
"callee": { "type": "Identifier", "name": "func" },
"arguments": [],
"optional": true
},
"property": { "type": "Identifier", "name": "bbb" },
"optional": true
}
Both (func?.())?.bbb
and func?.()?.bbb
shares the same AST structures because they are equivalent. Just like (func()).bbb
is same as func().bbb
.
// (func?.()).bbb
{
"type": "MemberExpression",
"object": {
"type": "OptionalCallExpression",
"callee": { "type": "Identifier", "name": "func" },
"arguments": [],
"optional": true
},
"property": { "type": "Identifier", "name": "bbb" }
}
However (func?.()).bbb
is not equivalent to func?.().bbb
. Because .bbb
is not a part of the optional chain, it is now a member expression.
// obj?.aaa?.()
{
"type": "OptionalCallExpression",
"callee": {
"type": "OptionalMemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" },
"optional": true
},
"arguments": [],
"optional": true
}
// obj?.aaa()
{
"type": "OptionalCallExpression",
"callee": {
"type": "OptionalMemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" },
"optional": true
},
"arguments": [],
"optional": false
}
()
is a part of optional chain, so it is OptionalCallExpression
, but it does not follow a ?.
token, so it has optional: false
setting.
// (obj?.aaa)?.()
{
"type": "OptionalCallExpression",
"callee": {
"type": "OptionalMemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" },
"optional": true
},
"arguments": [],
"optional": true
}
Both (obj?.aaa)?.()
and obj?.aaa?.()
shares the same AST structures because they are equivalent. Just like (obj.aaa)()
is same as obj.aaa()
.
// (obj?.aaa)()
{
"type": "CallExpression",
"callee": {
"type": "OptionalMemberExpression",
"object": { "type": "Identifier", "name": "obj" },
"property": { "type": "Identifier", "name": "aaa" },
"optional": true
},
"arguments": []
}
However (obj?.aaa)()
is not equivalent to obj?.aaa()
. Because ()
is not a part of the optional chain, it is now a call expression.
The PR of adding optional chaining to AST spec is under review: babel/babel#11729. Feel free to leave comments there.