-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Built-in Symbol.iterator methods return IterableIterator<T> extending Iterator<T>, not Iterator<T, undefined> #52998
Comments
Somewhat of a tangent but |
Thanks for the correction! I've fixed my original comment to be more accurate about this. |
Again, I don’t think you really want this. You can’t narrow |
Semantically, // type of iterateStr is Generator<string, void, undefined>, i.e. TReturn is void
function* iterateStr(str: string) {
yield* str
}
Seems to work fine to me? Playground declare let x: string | void
if (typeof x === 'string') {
x // string
}
let y = x! // string
let z: Exclude<typeof x, void> // string I'm not married to using |
The problem with More concretely, here's the issue with const foo: () => void = () => 777;
const bar: () => number = () => 1206;
const x: 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 |
Wow, Is the fact that union types like const fn: () => void = () => 1
let x: string | void = 2 > 1 ? fn() : 'str'
if (x) {
x.trim() // passes type checking but throws runtime error
} If this is considered a bug, is there a live issue for it where we can split of this discussion? |
Yeah, |
The same issue happens on const set = new Set<string>();
const v = set.values().next().value;
// should be string | undefined, but get any |
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 return?(value?: TReturn): IteratorResult<T, TReturn>;
throw?(e?: any): IteratorResult<T, TReturn>; That widens the discrepancy between compile-time type checking and runtime type guarantees even further: const arr = [1]
const iter = arr.values()
const next = iter.next()
if (!next.done) next.value // ✅ type = number
next.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) 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 |
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. Future versions of ECMAScript may also decide to add those methods, too, so people might be writing |
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. 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>;
}
// an existing library function that's always accepted `IterableIterator<number>`; signature remains unchanged
function someLibraryFn(iter: IterableIterator<number>) {
iter.throw?.()
iter.return?.()
return iter.next().value
}
declare const iter: BuiltInIterableIterator<number>
// works with no issues
// `BuiltInIterableIterator<number>` is still assignable to IterableIterator<number>
// type of `val` remains `any`
const val = someLibraryFn(iter) Anyhow, my PR #56517 is failing CI due to |
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 What I've settled on since I opened this issue is the following: let char = ''
for (const ch of str) {
char = ch
break
} 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 An alternative using an IIFE, which is even funkier, but has the advantage of allowing you to declare the variable with const char = (() => {
for (const ch of str) return ch
return ''
})() |
lib Update Request
The built-in
Symbol.iterator
methods returnIterableIterator<T>
extendingIterator<T>
, not the expectedIterator<T, undefined>
. For example:The expected type of
char
isstring | undefined
, but instead it'sany
. This isn't correct — it will never be anything other than a string (strictly a single Unicode char) orundefined
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:And then changing all the built-ins (
String
,Array
, etc) like so:Could that work as a fix and be sufficiently backward-compatible?
Configuration Check
My compilation target is
ES2020
and my lib isthe default
.Missing / Incorrect Definition
Various — any built-in JS type with a
Symbol.iterator
method, such asArray
,String
,Map
,Set
,Uint8Array
, etc.Sample Code
Documentation Link
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/@@iterator
The text was updated successfully, but these errors were encountered: