I have a problem in TypeScript with an IteratorResult destructuring. I'm doing a simple object destructuring and I'm getting an array destructuring error.
This is the code that is throwing the TypeScript error:
class ServiceLabelCollection extends ICollection<ServiceLabel> {
private collection: Collection<ServiceLabel>;
public addSoft(labels: ServiceLabel | Iterable<ServiceLabel>): this {
const isIterable = !(labels instanceof ServiceLabel);
const toAddIterable: Iterable<ServiceLabel> = isIterable ? labels : [labels];
const iterator: Iterator<ServiceLabel> = toAddIterable[Symbol.iterator]();
while (this.collection.size < ServiceLabelsCollection.maxSize) {
const { value, done }: IteratorResult<ServiceLabel> = iterator.next(); // Unsafe array destructuring of a tuple element with an `any` value. eslint@typescript-eslint/no-unsafe-assignment
if (done) break;
this.collection.addSoft(value);
}
return this;
}
}
The issue seems to be a bug, because it is an object destructuring, and I think it's well implemented. The other reason why I think it could be an TypeScript bug is due to the next code does not give the error:
class ServiceLabelCollection extends ICollection<ServiceLabel> {
private collection: Collection<ServiceLabel>;
public addSoft(labels: ServiceLabel | Iterable<ServiceLabel>): this {
const isIterable = !(labels instanceof ServiceLabel);
const toAddIterable: Iterable<ServiceLabel> = isIterable ? labels : [labels];
const iterator: Iterator<ServiceLabel> = toAddIterable[Symbol.iterator]();
while (this.collection.size < ServiceLabelsCollection.maxSize) {
const result: IteratorResult<ServiceLabel> = iterator.next(); // Simply I didn't do destructuring
if (result.done) break;
this.collection.addSoft(result.value);
}
return this;
}
}
Do someone see an error that I could be doing here? Or someone else guess that it could be a TypeScript bug?
TLDR: Don't manually type everything. It's ok to let typescript infer things. You've actually used the wrong types here which caused a type to be widened to any
, which eslint is now yelling at you about.
Unsafe array destructuring of a tuple element with an
any
value. eslint@typescript-eslint/no-unsafe-assignment
This is not a typescript error. It's an eslint error. And the root of the problem is that the type of result.value
is any
. The destructuring just causes eslint to notice that.
So the real problem is that result.value
is typed as any
. Let's fix that.
I've reduced your code to this:
type ServiceLabel = { label: string }
const array: ServiceLabel[] = []
const iterator = array[Symbol.iterator]()
const { value, done }: IteratorResult<ServiceLabel> = iterator.next();
// ^ any
Now in your IDE (or in the typescript playground) if you hover over IteratorResult
in that snippet, you will see this type:
type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>
Note that the second parameter defaults to any
when unspecified. And then note that you only passed one parameter.
Now hover over .next()
and you will see this type:
Iterator<ServiceLabel, undefined, unknown>.next(...[value]: [] | [unknown]):
IteratorResult<ServiceLabel, undefined>
Which says that iterator.next()
is returning this type:
IteratorResult<ServiceLabel, undefined>
Which is different than the type you chose. If you change your type to IteratorResult<ServiceLabel, undefined>
then everything should work as you expect.
const { value, done }: IteratorResult<ServiceLabel, undefined> = iterator.next();
Or just let it be inferred, because in typescript you should't be providing a type to every single value.
const { value, done } = iterator.next();
If you dive into the types for IteratorResult
(cmd/ctrl+click the type in your IDE) it paints a bigger picture:
interface IteratorYieldResult<TYield> {
done?: false;
value: TYield;
}
interface IteratorReturnResult<TReturn> {
done: true;
value: TReturn;
}
type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
An iterator has two ways it may return values. The "yield" result is what you typically expect the iterator to be iterating over, and the "return" result is the value for when the iterator is "done".
In the case of an ArrayIterator
, the "yield" type is the type of members of the array, and the "return" result is undefined
because when you are the end of the array, there's no items left to return.
But if you manually use an IteratorResult
type, you have to provide both types to avoid any
, because the TReturn
type defaults to any
. Which blows up this union type:
IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
// reduces to
IteratorYieldResult<T> | IteratorReturnResult<any>;
// reduces to
{ done?: false, value: T } | { done: true, value: any }
// If you access `value` of that union type, you get:
T | any
// which reduces to
any
Because any union with any
is any
. For example type A = number | any // any
.