In the TypeScript design goal it writes Emit clean, idiomatic, recognizable JavaScript code, but the optional chaining breaks this.
TypeScript Version: 3.7.0-beta
Search Terms: Optional chaining
Code
const a = globalThis?.browser?.runtime?.getBackgroundPage?.()?.location?.href ?? location.href
Expected behavior:
Generate a recognizable JavaScript code.
Actual behavior:
var _a, _b, _c, _d, _e, _f, _g, _h;
const a = (_h = (_g = (_f = (_e = (_c = (_b = (_a = globalThis) === null || _a === void 0 ? void 0 : _a.browser) === null || _b === void 0 ? void 0 : _b.runtime) === null || _c === void 0 ? void 0 : (_d = _c).getBackgroundPage) === null || _e === void 0 ? void 0 : _e.call(_d)) === null || _f === void 0 ? void 0 : _f.location) === null || _g === void 0 ? void 0 : _g.href, (_h !== null && _h !== void 0 ? _h : location.href));
Add like lodash.get function to helper?
example code:
function _isArray(value: any): value is any[] {
// target to es5
return Object.prototype.toString.call(value) === '[object Array]';
}
function _isNil(value: any): value is (undefined | null) {
return value == null || value === void 0;
}
function _get(object: any, paths: any[], defaultValue?: any) {
let index = 0;
let path: typeof paths[number];
let prev = object;
while (!_isNil(object) && index < paths.length) {
path = paths[index++];
object = _isArray(path) ? object.apply(prev, path) : object[path];
prev = object;
}
return (index && index == paths.length) ? object : defaultValue;
}
const foo = {
bar: {
apply(value: string) {
return value;
},
[Symbol.toStringTag]() {
return "tag";
}
}
}
// foo?.bar?.apply("sample")?.length ?? "default"
_get(foo, ["bar", "apply", ["sample"], "length"], "default")
// foo?.[Symbol.toStringTag]()?.length
_get(foo, ["bar", Symbol.toStringTag, [], "length"])
// foo.bar?.apply("sample").length
_get(foo.bar, ["apply", ["sample"]]).length
What is your target version?
target ES2017
The idiomatic and recognizable form is not spec compliant:
something && something.prop && something.prop.method && something.prop.method();
@ilogico as a example:
some[i++]?.large?.property?.paths
is not the same as:
some[i++] && some[i++].large && some[i++].large.property && some[i++].large.property.paths
The current output is always valid.
The idiomatic and recognizable form is not spec compliant:
something && something.prop && something.prop.method && something.prop.method();
That's not correct. If something.prop is a getter, you're invoking it multiple times. A more readable names is okay.
const something_ = !(sth === void 0 || sth === null) ? sth : void 0
const something_prop = !(something_ === void 0 || something_ === null) ? something_.prop : void 0
const something_prop_method = !(something_prop === void 0 || something_prop === null) ? something_prop.method : void 0
something_prop_method !== void 0 && something_prop_method !== null && Reflect.apply(something_prop_method, something_prop, [])
I think you misread, I'm not suggesting the transpilation is done like that. I'm stating the idiomatic way is not spec compliant.
Change your target to esnext.
Also, provide a playground next time. It helps poor mobile users like me test stuff more easily =(
If only the playground supported mobile users and use a regular textarea...
Change your target to esnext.
Also, provide a playground next time. It helps poor mobile users like me test stuff more easily =(
If only the playground supported mobile users and use a regular textarea...
Does the playground supports typescript@beta?
And it has no meaning to change the target to ESNext. I'm talking about how to translate it to old ES versions.
I'm on mobile, so typing on the playground and getting a link was a pain in the butt.
Playground TS 3.7, esnext
When you highlighted this,
Emit clean, idiomatic, recognizable JavaScript code
I thought you were complaining it wasn't emitting exactly what you typed, since it's technically clean, idiomatic and recognizable (Stage 3) JavaScript.
So, if that's what you were complaining about, changing your target to esnext would be the solution.
But your actual complaint is that it doesn't look like it's emitting clean, idiomatic and recognizable es2017 JavaScript, when trying to down level its emit.
Sorry if I'm being pedantic =x
Personal opinion, though, both your desired emit and the one TS currently outputs are basically unreadable to me (if we're debating whether it's clean, idiomatic, and recognizable)
@rbuckton
That sure is a lot of ?. in the example line; you only need ?. on the properties you expect might be null/undefined, not on everything. It short-circuits.
There's no way to emit this in a way that "would be readable" while still preserving order of operations. Like, it is _possible_, but especially when embedded in any expression more complicated than an assignment expression, it would require also transforming a bunch of unrelated code and spilling even more things to local variables to preserve behavior.
Say, what if you have an optional chain in the argument to a method call.
foo.bar(foo.baz?.x)
Maybe typescript could spill the foo.baz?.x into a separate declaration, say, _foo_baz_x = /* ... */, but that would mean potentially invoking baz and x's getter (or get Proxy trap) before foo.bar. To avoid that, foo.bar would also have to be spilled into a _foo_bar local, and _foo_bar's call expression would have to be converted into a Function.prototype.call.call call expression.
var _foo_bar = foo.bar
var _foo_baz = foo.baz
var _foo_baz_x = _foo_baz !== null && _foo_baz !== undefined ? _foo_baz.x : undefined
Function.prototype.call.call(_foo_bar, foo, _foo_baz_x)
This is probably the easy case; what do you do if it's already in the consequent/alternate of a ternary expression, say, checkX ? foo.baz?.x : ''; every single sub-expression could/should only be evaluated if checkX is truthy to begin with.
At best, TypeScript could try to derive the spilled locals from the identifiers in the subexpressions, kinda like I did in my code example, instead of just using _a, _b, etc. They'll all get crushed when the code is minified anyway, there's no need to emit short local identifiers.
The helper described in the https://github.com/microsoft/TypeScript/issues/33737#issuecomment-537312950 can resolve the order problem I think
While I can understand that the output JavaScript is harder to read, this is no worse than our emit for destructuring or computed properties. This emit emulates the correct runtime behavior and is the most efficient representation. A helper like _get that you have above would not preserve the correct execution order for element access in a?.b[c()], nor would it work for a chain involving an optional call without falling back to the current emit: a?.b(e()).c.d.
I'm closing this as "Working as Intended". While it is always a goal to emit clean, idiomatic JavaScript when possible, this is not always feasible or recommended. This is a case where we have chosen to as closely match the semantics of the proposal as possible.
Hi, @rbuckton !
I wrote a helper that makes the code more readable and using it produces less code.
You may see it here http://jsfiddle.net/sjmkz81b/4/
It works like maybe monad, but it is light version for ?. and ??.
No need to generate temporary variables like _a, _b, _c
It works on demand - if null or undefined occurs in the chain, then further all calculations are stopped and the undefined is passed along the chain, so index access with index incrementation works fine.
Please check feedle for tests.
Of the minuses - the stack trace is bigger and the trace is probably slower.
Although I did not take measurements.
What do you think about that?
a?.[i++]?.c?.d?.(1, 2, 3) ?? 123
// becomes
// es6
__helper(a)(x => x[i++])(x => x.c)(x => x.d, [1, 2, 3])(() => 123)
// es5
__helper(a)(function(x) { return x[i++]; })(function(x) { return x.c; })(function(x) { return x.d; }, [1, 2, 3])(function() { return 123; })
// current version on playground
var _a, _b, _c, _d; _d = (_c = (_b = (_a = a) === null || _a === void 0 ? void 0 : _a[i++]) === null || _b === void 0 ? void 0 : _b.c) === null || _c === void 0 ? void 0 : _c.d, (_d !== null && _d !== void 0 ? _d : 123);
```js
a ?? 123
// becomes
// es6
__helper(a)(() => 123)
// es5
__helper(a)(function() { return 123 })
// current version on playground
(a !== null && a !== void 0 ? a : 123)
```js
a?.b
// es6
__helper(a)(x => x.b)()
// es5
__helper(a)(function(x) { return x.b })()
// current version on playground
var _a; (_a = a) === null || _a === void 0 ? void 0 : _a.b;
```js
// helper itself
function __helper(x) {
return function(fn, args, _x = x) {
if (_x !== void 0 && _x !== null) {
if (fn === void 0) { // __h({ y: 1 })() => { y: 1 }
return _x;
}
if (fn.length) { // get property __h({ y: 1 })(x => x.y) => __helper([[maybeValue]])
if (args === void 0) {
return __helper(fn(_x));
} else {
// get property and call __h({ y: 1 })(x => x.y, [1, 2, 3]) => __helper([[maybeValue]])
return __helper(fn(_x).apply(_x, args));
}
} else { // get or default __h(1)(() => 2) => 1
return _x;
}
}
if (fn === void 0) { // __h(undefined | null)() => undefined
return void 0;
}
if (fn.length) { // get property [[undefined]]?.y __h(undefined | null)(x => x.y) => __helper(undefined | null)
return __helper(_x);
} else { // get or default __h(undefined | null)(() => 2) => 2
return fn();
}
}
```js
// minified version
function h(i) {return function(e, n, l) {return void 0 === l && (l = i), null != l ? void 0 === e ? l : e.length ? h(void 0 === n ? e(l) : e(l).apply(l, n)) : l : void 0 === e ? void 0 : e.length ? h(l) : e()}}
Most helpful comment
I'm closing this as "Working as Intended". While it is always a goal to emit clean, idiomatic JavaScript when possible, this is not always feasible or recommended. This is a case where we have chosen to as closely match the semantics of the proposal as possible.