It's common to declare a variable only to immediately mutate it in a following statement:
let result = 3
if (conditionA) result = 1
else if (conditionB) result = 2
This is typical JavaScript because support for conditional expressions that yield values is limited. But it's not ideal -- now we have a mutable result
variable despite never planning to mutate it again. An existing alternative is the ternary expression, but it quickly becomes hard to read when nested:
const result = conditionA ? 1 : (conditionB ? 2 : 3)
What if we could combine these two concepts, yielding a value directly from our if
statement? This would combine the best of both options, maximizing readability while avoiding mutation:
const result =
if (conditionA) yield 2
else if (conditionB) yield 3
else yield 1
There's no reason we have to stop at conditionals. What if we could yield Array
s directly from loops? We could effectively combine the concepts of mapping, filtering, and flattening without having to introduce an entirely new syntax for comprehensions:
const newUsers = for (const user of users) if (user.age > 5) yield user.id
Flattening comes naturally:
const newUsers =
for (const users of userGroups)
for (const user of user)
if (user.age > 5) yield user.id
The concept also works asynchronously:
const resultsPromise =
for await (const response of requests)
if (response.success) yield response.data
Because they are just expressions they can be used anywhere an expression can be used, like as the return value for arrow functions:
const allJson = async requests =>
for await (const response of requests)
if (response.ok) yield await response.json()
Or in JSX expressions:
const ListOfItems = ({items}) =>
<ul>
{ for (const item of items) if (item.isVisible) yield <li>{ item.name }</li> }
</ul>
Specification
The 'rules' around this concept would be fairly straightforward:
- A conditional or loop statement is 'yielding' when used where an expression is expected.
- A yielding conditional resolves to the yielded value and stops executing when a
yield
statement is encountered. If none are encountered, the expression resolves toundefined
. Ayield*
statement is aSyntaxError
in a yielding conditional. - A synchronous yielding loop always resolves to an
Array
. When ayield
statement is encountered, the yielded value is appended to the array and execution continues. Ayield*
statement individually yields each value in the yielded iterable. If noyield
statements are encountered, the expression resolves to[]
. - An asynchronous yielding loop (
for await...of
) always resolves toPromise<Array>
. Otherwise it acts similarly to a synchronous yielding loop.
Alternative syntax
An alternative version of this proposal would be to introduce a new syntax for these expressions rather than inferring whether or not a statement is a yielding expression based on context. We could introduce new keywords inspired by generator function syntax, like for*
:
const newUsers = for* (const user of users) if (user.age > 5) yield user.id
This has the advantage of being more explicit and better distinguishing between expressions & statements, but the concept is trickier when thinking about conditionals. You could have if*
, but then what about else
? Maybe the whole expression is a generator conditional if it starts with if*
:
const result =
if* (conditionA) yield 2
else if (conditionB) yield 3
else yield 1
I don't particularly love this since it's easy to lose track in the later branches.
Prior art
This proposal is heavily inspired by Scala. In Scala (and in Ruby), all statements are value-yielding expressions. if
statements always yield the result of the last executed expression. This concept is nice but doesn't fit very well with JavaScript design patterns which tend to be more explicit (ie, these languages also both support implicit returns). So I think yield
works better for JavaScript as the more explicit option.
Yielding loops are very similar to Scala's 'for comprehensions', and result in a very similar syntax.