Is there a way to force a consumer to use an entire iterator?
For example:
const [first] = tuple // throws "Need to use both"
const [first, second] = tuple // works
I was hoping something like:
*[Symbol.iterator]() {
const tuple = this.error
? ([null, this.error] as const)
: ([this.value, null] as const);
yield tuple[0];
return {
done: true,
value: (() => {
throw new Error("Must destructure both values from tuple");
})(),
};
}
Would work, but doesn't. Is this even possible? I can't really think of a solid way but would love some help from the big brains.
Is there a way to force a consumer to use an entire iterator?
For example:
const [first] = tuple // throws "Need to use both"
const [first, second] = tuple // works
I was hoping something like:
*[Symbol.iterator]() {
const tuple = this.error
? ([null, this.error] as const)
: ([this.value, null] as const);
yield tuple[0];
return {
done: true,
value: (() => {
throw new Error("Must destructure both values from tuple");
})(),
};
}
Would work, but doesn't. Is this even possible? I can't really think of a solid way but would love some help from the big brains.
Share Improve this question asked Feb 14 at 0:08 Justin WallaceJustin Wallace 1028 bronze badges 01 Answer
Reset to default 3It is possible but I'm not sure why you're doing this. It's time to abuse finally
to mess up the control flow if you really insist.
const tuple = {
*[Symbol.iterator]() {
let hasError = true;
try {
yield 1;
hasError = false;
yield 2;
} finally {
if(hasError) {
throw new Error('Must destructure both values from tuple');
}
}
}
}
const [first] = tuple; // Error: Must destructure both values from tuple
console.log(first);
const tuple = {
*[Symbol.iterator]() {
let hasError = true;
try {
yield 1;
hasError = false;
yield 2;
} finally {
if(hasError) {
throw new Error('Must destructure both values from tuple');
}
}
}
}
const [first, second] = tuple; // works fine
console.log(first, second);
Why It Works
According to try...catch
Control flow statements (
return
,throw
,break
,continue
) in thefinally
block will "mask" any completion value of thetry
block orcatch
block. In this example, thetry
block tries toreturn 1
, but before returning, the control flow is yielded to thefinally
block first, so thefinally
block's return value is returned instead.function doIt() { try { return 1; } finally { return 2; } } doIt(); // returns 2
finally
is a bit cursed (but makes sense), it always executes when exiting a try
block, regardless of how the exit happens.
Bare that in mind and let's disect this statement:
const [first] = tuple
tuple
is referenced, which is the object with an iterator.const [first]
is executed, it's a destructuring statement, that causes*[Symbol.iterator]()
to run, and requesting for exactly one value.
let hasError = true;
try {
yield 1;
hasError = false;
yield 2;
} finally {
if(hasError) {
throw new Error('Must destructure both values from tuple');
}
}
After
yield 1
is executed, the requested quantity of values (one) has met, so the generator function is halt and ready to exit, but before exiting thetry
body, thefinally
has to run.Because it hasn't reached
hasError = false
part, theif(hasError)
resolvestrue
thus the error is thown before the function truly exits.
However on step 2, if the statement was const [first, second]
, it's requesting two values instead. So it will not try to exit the try
body after yield 1
, so hasError = false
has reached, thus when the finally
block is executed after yield 2
, if(hasError)
will fail and the error will no longer be thrown.