Deprecate tryInvoke
Summary
Deprecate support for tryInvoke
in Ember's Utils module (@ember/utils) because native JavaScript has optional chaining for developers to use as an alternative solution. Deprecating tryInvoke
will help to reduce Ember API redundancy.
Motivation
In most cases, Function arguments should not be optional, but in the rare occasion that a Function argument is intentionally optional by design, we can use native JavaScript's optional chaining as a solution. Deprecating tryInvoke
will help to reduce Ember API redundancy.
Transition Path
Ember will start logging deprecation messages for tryInvoke
usage. Deprecation text: Using tryInvoke has been deprecated. Instead, consider using native JavaScript optional chaining.
We can codemod our current usage of tryInvoke
with the equivalent behaviour using plain JavaScript. The migration guide will cover this example:
Before:
import { tryInvoke } from '@ember/utils';
foo() {
tryInvoke(this.args, 'bar', ['baz']);
}
After:
foo() {
this.args.bar?.('baz');
}
Using Optional Chaining Operator
The optional chaining operator ?.
permits reading the value of a property located deep within a chain of connected objects without having to expressly validate that each reference in the chain is valid. The ?.
operator functions similarly to the .
chaining operator, except that instead of causing an error if a reference is nullish (null
or undefined
), the expression short-circuits with a return value of undefined
. When used with Function calls, it returns undefined
if the given function does not exist:
const adventurer = {
name: 'Alice',
cat: {
name: 'Dinah'
}
};
const dogName = adventurer.dog?.name;
console.log(dogName);
// expected output: undefined
console.log(adventurer.someNonExistentMethod?.());
// expected output: undefined
Tooling Support:
Babel already supports the optional chaining operator so we can use that for future use-cases.
ember-cli-babel all recent versions support optional chaining operator.
TypeScript, similarly, as of version 3.7 also supports the operator so we will not be breaking that flow either.
How We Teach This
Add to Ember Deprecation Guide
In the Ember Deprecation Guide we will add the following text:
Deprecate support for tryInvoke
in Ember's Utils module (@ember/utils). In most cases, Function arguments should not be optional, but in the rare occasion that a Function argument is intentionally optional by design, we can use native JavaScript's optional chaining as a solution. Deprecating tryInvoke
will help to reduce Ember API redundancy.
Before:
import { tryInvoke } from '@ember/utils';
foo() {
tryInvoke(this.args, 'bar', ['baz']);
}
After:
foo() {
this.args.bar?.('baz');
}
Remove from API docs
The references to tryInvoke
will need to be removed from the API docs.
Add to Ember Guides
In Ember Guides under the Arguments section, we will create 2 new sub-headings called Function Arguments
and Optional Function Arguments
:
Function Arguments
Arguments passed into components can be of type Function. In most cases, Function arguments should be treated as required arguments and therefore should be invoked with normal Function invocation ()
. It is important to intentionally treat Function arguments as required because in the off chance that the Function argument is undefined
, normal Function invocation ()
will cause a runtime exception and produce a stack trace, making it easier for the developer to find the root cause of the bug.
// app/components/parent.js
@action
fooParent() {
// ...
}
{{!-- app/components/parent.hbs --}}
<Child @bar={{this.fooParent}} />
// app/components/child.js
fooChild() {
this.args.bar('baz');
}
Optional Function Arguments
In the rare occasion that a Function argument is intentionally optional by design, you can use native JavaScript's optional chaining to invoke the optional Function argument ?.()
. We want to avoid unintentionally treating Function arguments as optional because optional chaining invocation has the side effect of failing silently with no stack trace logged. This will cause a difficult debugging experience for the developer.
{{!-- app/components/parent.hbs --}}
<Child @bar={{this.fooParent}} />
{{!-- app/components/some-other-parent.hbs --}}
<Child />
// app/components/child.js
fooChild() {
this.args.bar?.('baz');
}
Drawbacks
This change will cause some deprecation noise but could be mitigated with a codemod.
Alternative Solutions
We could check that the Function name exists on the object before invocation using an if
block, but this alternative leaves the developer to have to wrap each Function call in an if
block, making this pattern very cumbersome.
foo() {
if (typeof this.args.bar === 'function') {
this.args.bar('baz');
}
}
Alternatives
Do nothing
We could keep support in place, and provide more guidance around using it.
Unresolved questions
None at the moment.