Pesquisa de site

arrays - Como posso determinar se um Iterable estará vazio ou não após iterar em TypeScript?


Como posso diferenciar entre iteráveis que se tornam vazios após a iteração e aqueles que não, em TypeScript?

type ReusableIterable<T> = Iterable<T> // ?

function iterate(iterable: ReusableIterable<any>) {
    // Do something with iterable
    console.log(Array.from(iterable))
}

// a: number[]
const a = [1, 2, 3]
iterate(a) // [1, 2, 3]
iterate(a) // [1, 2, 3]

// b: () => Generator<number>
const b = function*() {
    yield* [1, 2, 3]
}
iterate(b()) // [1, 2, 3]
iterate(b()) // [1, 2, 3]

// c: Generator<number>
const c = b()
iterate(c) // [1, 2, 3]
iterate(c) // []

// d: IterableIterator<number>
const d = [1, 2, 3].values()
iterate(d) // [1, 2, 3]
iterate(d) // []

Resposta #1

TypeScript não tem uma boa forma de distinguir iteráveis "reutilizáveis" de iteráveis "de uma única vez". O sistema de tipos do TypeScript geralmente considera os tipos como sem estado. Há algum suporte para restringir e expandir tipos com base no fluxo de controle de um programa, mas esse suporte funciona apenas em circunstâncias específicas e não acompanha mutações arbitrárias. Existe uma solicitação de recurso aberta há muito tempo em microsoft/TypeScript#16148 para suportar os chamados tipos afins, que representam recursos que podem ser usados no máximo uma vez. Se tivéssemos tipos afins em TypeScript, então você poderia possivelmente codificar iteráveis de uma vez com um tipo afim e os reutilizáveis com um tipo normal. Mas não os temos (e duvido que algum dia teremos).

Então, isso é, a uma primeira aproximação, uma causa perdida.


Uma solução alternativa que você pode adotar é criar um alias de tipo para todos os tipos que você sabe que podem ser reutilizados, e então aceitar apenas esses:

type ReusableIterable =
    ReadonlyArray<any> | ReadonlyMap<any, any> | 
    ReadonlySet<any> | string // | ...

function iterate<T>(iterable: ReusableIterable & Iterable<T>) {
    console.log(Array.from(iterable))
}

const a = [1, 2, 3]
iterate(a) // okay

const b = function* () { yield* [1, 2, 3] }
iterate(b()) // error

const c = b()
iterate(c) // error

const d = [1, 2, 3].values()
iterate(d) // error

Isso necessariamente exclui quaisquer iteráveis reutilizáveis que você não conheça antecipadamente, e também permite quaisquer tipos que se conformem estruturalmente a um destes, mas que só possam ser usados uma vez (por exemplo, uma subclasse de Array que consome os elementos quando você os lê). E definitivamente não consegue diferenciar entre um iterável de uma vez "novo" e um iterável "usado". Mas provavelmente é o mais próximo do melhor que você pode fazer.

Observe que isso equivale a dizer "apenas aceite Array e force os chamadores a usar Array.from() por conta própria. O que não é ótimo, mas pelo menos tem uma alta probabilidade de funcionar.

Link do playground para o código