You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The built-in Symbol.iterator methods return IterableIterator<T> extending Iterator<T>, not the expected Iterator<T, undefined>. For example:
constchar=string[Symbol.iterator]().next().value
The expected type of char is string | undefined, but instead it's any. This isn't correct — it will never be anything other than a string (strictly a single Unicode char) or undefined at runtime.
This looks to be related to #33353, which was closed due to backwards-compatibility concerns; however, it could presumably fixed without changing the default types of IterableIterator, instead changing its definition to take an extra type parameter:
Could that work as a fix and be sufficiently backward-compatible?
Configuration Check
My compilation target is ES2020 and my lib is the default.
Missing / Incorrect Definition
Various — any built-in JS type with a Symbol.iterator method, such as Array, String, Map, Set, Uint8Array, etc.
Sample Code
constchar=string[Symbol.iterator]().next().value// expected type: string | undefined// actual type: anyconstdefinitelyChar=nonEmptyString[Symbol.iterator]().next().value!// expected type: string// actual type: any
Again, I don’t think you really want this. You can’t narrow void out of a union. If your argument is that the value can be undefined then you should use undefined explicitly.
If your argument is that the value can be undefined then you should use undefined explicitly.
Semantically, string | void makes more sense to me than string | undefined, by analogy with generator functions that don't explicitly return something:
// type of iterateStr is Generator<string, void, undefined>, i.e. TReturn is voidfunction*iterateStr(str: string){yield*str}
The problem with string | void, is that void is an implicit promise that the code receiving a value of that type won't access it at all. More critically, () => T is assignable to () => void for all T, which means the value is not even guaranteed to be undefined. string | void is thus conceptually unsound because you have to access the value to narrow it.
More concretely, here's the issue with void:
constfoo: ()=>void=()=>777;constbar: ()=>number=()=>1206;constx: number|void=Math.random()>0.5 ? foo() : bar();// note: by calling foo which has a void return type, we've promised to discard its return value.// but now there's a problem: we have no way to know whether x is garbage or not!
In other words, in the context of larger union type it's basically just an any with extra steps.
changed the title [-]Built-in Symbol.iterator methods return IterableIterator<T> extending Iterator<T>, not Iterator<T, void>[/-][+]Built-in Symbol.iterator methods return IterableIterator<T> extending Iterator<T>, not Iterator<T, undefined>[/+]on Mar 1, 2023
Yeah, void is an interesting animal. Truthiness checks can sometimes narrow it away but they really shouldn’t. All the pitfalls are discussed in the issue I linked above: #42709
Having thought some more about it though, maybe it'd be better to have an entirely new interface that doesn't extend Iterator. In addition to defaulting TReturn to any, Iterator also has two methods that are never present on the built-in iterable iterators:
That widens the discrepancy between compile-time type checking and runtime type guarantees even further:
constarr=[1]constiter=arr.values()constnext=iter.next()if(!next.done)next.value// ✅ type = numbernext.value// ❌ type = any (should be number | undefined)iter.nonExistentMethod?.()// ✅ Property 'nonExistentMethod' does not exist on type 'IterableIterator<number>'.iter.return?.()// ❌ TS allows this, even though the `return` method will never exist (absent monkey-patching etc)iter.throw?.()// ❌ TS allows this, even though the `throw` method will never exist (absent monkey-patching etc)
Not sure if BuiltInIterableIterator is the best name though — maybe GenericIterableIterator (not to be confused with TS generics) or something else? Some other options: StrictIterableIterator, ImplicitReturnIterableIterator, StandardIterableIterator
Not sure I agree on the second point re: removing the nonexistent methods simply because they never exist. Code may want to deal with iterators generically and not have to care whether it’s a built-in iterator or not. iter.return?.() should still be allowed, IMO.
Future versions of ECMAScript may also decide to add those methods, too, so people might be writing iter.return?.()today, just for the sake of future-proofing.
Not sure I agree on the second point re: removing the nonexistent methods simply because they never exist. Code may want to deal with iterators generically and not have to care whether it’s a built-in iterator or not. iter.return?.() should still be allowed, IMO.
I'm not suggesting function parameter types in library code should be changed, only the types of the built-in iterables themselves. Userland code, on the other hand, can be as strict or lenient as it likes.
interfaceIterator<T,TReturn=any,TNext=undefined>{next(...args: []|[TNext]): IteratorResult<T,TReturn>;return?(value?: TReturn): IteratorResult<T,TReturn>;throw?(e?: any): IteratorResult<T,TReturn>;}interfaceIterableIterator<T>extendsIterator<T>{[Symbol.iterator](): IterableIterator<T>;}interfaceBuiltInIterableIterator<T>{next(): IteratorResult<T,undefined>;[Symbol.iterator](): BuiltInIterableIterator<T>;}// an existing library function that's always accepted `IterableIterator<number>`; signature remains unchangedfunctionsomeLibraryFn(iter: IterableIterator<number>){iter.throw?.()iter.return?.()returniter.next().value}declareconstiter: BuiltInIterableIterator<number>// works with no issues// `BuiltInIterableIterator<number>` is still assignable to IterableIterator<number>// type of `val` remains `any`constval=someLibraryFn(iter)
Anyhow, my PR #56517 is failing CI due to Type 'void' is not assignable to type 'undefined'. Per the discussion earlier in this issue thread, I don't think typing next's return type as IteratorResult<T, void>; is an appropriate solution; in which case, it would seem like this fix is blocked pending #36239.
As this may not get fixed any time soon, I'll leave this here too: my main use case for this is wanting an easy way to grab the first element from an iterable (often the first UTF-8 char of a string). The most ergonomic way is [...str][0], but iterating the whole string just to get the first item seems pretty wasteful. str[Symbol.iterator]().next().value is only a shade more verbose, but loses all type information in the process due to this issue.
What I've settled on since I opened this issue is the following:
letchar=''for(constchofstr){char=chbreak}
It looks a little funky, but it's reasonably concise, performant (doesn't iterate the whole string), correctly typed, and also has the advantage of forcing you to choose a default (in this case '') in case the string is empty.
An alternative using an IIFE, which is even funkier, but has the advantage of allowing you to declare the variable with const and/or allowing a different type for the default value without explicitly declaring it:
Activity
fatcerberus commentedon Feb 27, 2023
Somewhat of a tangent but
voidis not a synonym forundefinedand shouldn’t be treated as such. See #42709.lionel-rowe commentedon Feb 27, 2023
Thanks for the correction! I've fixed my original comment to be more accurate about this.
fatcerberus commentedon Feb 27, 2023
Again, I don’t think you really want this. You can’t narrow
voidout of a union. If your argument is that the value can beundefinedthen you should useundefinedexplicitly.lionel-rowe commentedon Feb 28, 2023
Semantically,
string | voidmakes more sense to me thanstring | undefined, by analogy with generator functions that don't explicitly return something:Seems to work fine to me? Playground
I'm not married to using
voidoverundefinedif the latter will prevent some class of bugs, though.fatcerberus commentedon Feb 28, 2023
The problem with
string | void, is thatvoidis an implicit promise that the code receiving a value of that type won't access it at all. More critically,() => Tis assignable to() => voidfor allT, which means the value is not even guaranteed to beundefined.string | voidis thus conceptually unsound because you have to access the value to narrow it.More concretely, here's the issue with
void:In other words, in the context of larger union type it's basically just an
anywith extra steps.[-]Built-in Symbol.iterator methods return IterableIterator<T> extending Iterator<T>, not Iterator<T, void>[/-][+]Built-in Symbol.iterator methods return IterableIterator<T> extending Iterator<T>, not Iterator<T, undefined>[/+]lionel-rowe commentedon Mar 1, 2023
Wow,
voidis a lot weirder than I thought... I've reworded the OP to useundefinedexclusively.Is the fact that union types like
string | voidcan be checked for truthiness considered a bug, then? It certainly seems like one:If this is considered a bug, is there a live issue for it where we can split of this discussion?
fatcerberus commentedon Mar 1, 2023
Yeah,
voidis an interesting animal. Truthiness checks can sometimes narrow it away but they really shouldn’t. All the pitfalls are discussed in the issue I linked above: #42709MuTsunTsai commentedon May 16, 2023
The same issue happens on
Set.prototype.values()as well:Playground
lionel-rowe commentedon Nov 22, 2023
Draft PR up here: #56502
Having thought some more about it though, maybe it'd be better to have an entirely new interface that doesn't extend
Iterator. In addition to defaultingTReturntoany,Iteratoralso has two methods that are never present on the built-in iterable iterators:That widens the discrepancy between compile-time type checking and runtime type guarantees even further:
Instead, we could have something like this:
interface Iterator<T, TReturn = any, TNext = undefined> { next(...args: [] | [TNext]): IteratorResult<T, TReturn>; return?(value?: TReturn): IteratorResult<T, TReturn>; throw?(e?: any): IteratorResult<T, TReturn>; } interface IterableIterator<T> extends Iterator<T> { [Symbol.iterator](): IterableIterator<T>; } + interface BuiltInIterableIterator<T> { + next(): IteratorResult<T, undefined>; + [Symbol.iterator](): BuiltInIterableIterator<T>; + } interface Array<T> { - [Symbol.iterator](): IterableIterator<T>; + [Symbol.iterator](): BuiltInIterableIterator<T>; - entries(): IterableIterator<[number, T]>; + entries(): BuiltInIterableIterator<[number, T]>; - keys(): IterableIterator<number>; + keys(): BuiltInIterableIterator<number>; - values(): IterableIterator<T>; + values(): BuiltInIterableIterator<T>; }Not sure if
BuiltInIterableIteratoris the best name though — maybeGenericIterableIterator(not to be confused with TS generics) or something else? Some other options:StrictIterableIterator,ImplicitReturnIterableIterator,StandardIterableIteratorfatcerberus commentedon Nov 24, 2023
Not sure I agree on the second point re: removing the nonexistent methods simply because they never exist. Code may want to deal with iterators generically and not have to care whether it’s a built-in iterator or not.
iter.return?.()should still be allowed, IMO.Future versions of ECMAScript may also decide to add those methods, too, so people might be writing
iter.return?.()today, just for the sake of future-proofing.lionel-rowe commentedon Nov 24, 2023
I'm not suggesting function parameter types in library code should be changed, only the types of the built-in iterables themselves. Userland code, on the other hand, can be as strict or lenient as it likes.
Anyhow, my PR #56517 is failing CI due to
Type 'void' is not assignable to type 'undefined'.Per the discussion earlier in this issue thread, I don't think typingnext's return type asIteratorResult<T, void>;is an appropriate solution; in which case, it would seem like this fix is blocked pending #36239.lionel-rowe commentedon Nov 24, 2023
As this may not get fixed any time soon, I'll leave this here too: my main use case for this is wanting an easy way to grab the first element from an iterable (often the first UTF-8 char of a string). The most ergonomic way is
[...str][0], but iterating the whole string just to get the first item seems pretty wasteful.str[Symbol.iterator]().next().valueis only a shade more verbose, but loses all type information in the process due to this issue.What I've settled on since I opened this issue is the following:
It looks a little funky, but it's reasonably concise, performant (doesn't iterate the whole string), correctly typed, and also has the advantage of forcing you to choose a default (in this case
'') in case the string is empty.An alternative using an IIFE, which is even funkier, but has the advantage of allowing you to declare the variable with
constand/or allowing a different type for the default value without explicitly declaring it: