javascript - Cómo escribir los resultados de un iterador usando generadores en typescript
Estoy usando TypeScript y creé el siguiente código
function* myIter(): Iterator<number> {
yield 3;
yield 2;
yield 1;
}
const iter: Iterator<number> = myIter();
const item:IteratorYieldResult<number> = iter.next();
DEMOSTRACIÓN
Produce un error de TypeScript
Decirme al pasar el cursor cuál es el error
Revisé qué está devolviendo exactamente next y encontré esta definición de interfaz:
interface Iterator<T, TReturn = any, TNext = undefined> {
// NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
return?(value?: TReturn): IteratorResult<T, TReturn>;
throw?(e?: any): IteratorResult<T, TReturn>;
}
Y este es el punto donde me pierdo, porque mi iterador está definido como Iteractor<number>, entonces, ¿qué es TReturn? Por otro lado, por defecto está establecido en any, así que supongo que es opcional y puedo omitirlo. Pero por alguna razón no funciona así con IteratorResult. De todos modos, se agradecería algo de orientación.
No estoy seguro de cuál es el problema aquí, pero tal vez alguien pueda ayudarme con esto.
Respuesta #1
Como has demostrado, el método next() de un iterador devuelve un valor de tipo IteratorResult. Esto se define como:
type IteratorResult<T, TReturn = any> = IteratorYieldResult<T>
| IteratorReturnResult<TReturn>;
, una unión de IteratorYieldResult y IteratorReturnResult, definidos a su vez así:
interface IteratorYieldResult<TYield> {
done?: false;
value: TYield;
}
interface IteratorReturnResult<TReturn> {
done: true;
value: TReturn;
}
Eso significa que el compilador te está diciendo que iter.next() está devolviendo o bien un objeto IteratorYieldResult con una propiedad done false/faltante, y un value de tipo number, o bien está devolviendo un objeto IteratorReturnResult con una propiedad done true, y un value de tipo any.
Estás asignando eso a una variable de tipo IteratorYieldResult, y el compilador te está diciendo "espera, podría no ser de ese tipo. Podría ser un IteratorReturnResult".
Ahora, dado que escribiste el generador tú mismo, sabes que la primera vez que llamas a next() vas a obtener un IteratorYieldResult porque hay tres sentencias yield antes de que la función devuelva un valor. Pero esa es información que simplemente el sistema de tipos no tiene. Es así:
function foo(): number | string {
return "hello";
}
const str: string = foo(); // error! number not assignable to string
La función foo() devuelve ya sea un número o un string, según su firma de tipo. Puedes saber que siempre devolverá un string, pero el compilador no puede darse cuenta, porque solo está mirando la firma, no profundizando en la implementación. Y por eso se queja: "oye, tal vez str sea un número en tiempo de ejecución, no un string como estás afirmando".
Entonces, ¿cuál es la cosa "correcta" que hacer aquí? Supongo que depende de lo que estés tratando de hacer. En este ejemplo de juguete, puedes simplemente decirle al compilador que no necesita preocuparse y que tú estás seguro de obtener un IteratorYieldResult y no un IteratorReturnResult. Y este tipo de "decirle" al compilador que algo es de un tipo más específico de lo que puede verificar se hace mediante una aserción de tipo:
const itemAsserted = iter.next() as IteratorYieldResult<number>; // okay
O, puedes hacer la verificación general donde no afirmas saber qué va a salir (lo cual tiene sentido si la implementación de myIter() podría cambiar, o si estás haciendo iter.next() en un bucle):
const itemSafe = iter.next();
// const itemSafe: IteratorResult<number, any>
if (!itemSafe.done) {
itemSafe; // const itemSafe: IteratorYieldResult<number>
itemSafe.value.toFixed(); // okay
}
Aquí simplemente dejamos que el compilador nos diga que iter.next() devuelve un IteratorResult<number, any>. Luego tomamos ese valor devuelto y lo probamos comprobando la propiedad done. Si no es true, entonces el compilador amablemente reduce el resultado a IteratorYieldResult<number> para ti. No necesitas afirmar nada porque el compilador entiende el tipo de verificación de tipos realizada anteriormente; eso se debe a que IteratorResult es una unión discriminada, donde una sola propiedad (en este caso done) puede ser verificada para averiguar a qué miembro de la unión estás mirando.
Está bien, espero que eso tenga sentido para ti y te sea útil. ¡Buena suerte!
Enlace de Playground al código
Respuesta #2
No me gustaba tener que comprobar done en cada lugar donde se llama, ya que sé exactamente cuántos elementos debería contener mi flujo, así que escribí un pequeño envoltorio que simplifica el valor de retorno de next():
export class SimpleIterator<T> {
constructor(private generator: AsyncGenerator<T>) {
}
async next(): Promise<T> {
const {value, done} = await this.generator.next()
if(done) throw new Error("Reached end of stream")
return value
}
}
Ahora puedes hacerlo
const iter = new simpleIterator(getSomeAsyncGenerator())
const value = await iter.next()
No es un verdadero "iterador" en el sentido de que no implementa la interfaz de iterador, por lo que no puedes usarlo en un for, pero de todos modos no era así como quería usarlo (los generadores ya se pueden usar en for, así que solo úsalo directamente si quieres eso)

