Search code examples
typescriptgenerator

How to apply function type to generator?


I have the following generator function:

async function * filterIterable (iter, predicate) {
  let i = 0
  for await (const val of iter) {
    if (predicate(val, i++)) {
      yield val
    }
  }
}

that I would like to type for the following two cases:

// type predicate
const nonNullable = <T>(val: T | undefined | null): val is T =>
  val !== undefined && val !== null

async function * gen1 () {
  yield 1
  yield 2
  yield 3
}

async function * gen2 () {
  yield 1
  yield undefined
  yield 3
}

const it1 = filterIterable(gen1(), n => n % 2 === 0)
const it2 = filterIterable(gen2(), nonNullable) // should be AsyncIterable<1 | 2>

I came up with this interface:

interface FilterIterable {
  <T> (
    iter: AsyncIterable<T>,
    predicate: (val: T, index: number) => boolean,
  ): AsyncIterable<T>;
  <T, S extends T> (
    iter: AsyncIterable<T>,
    predicate: (val: T, index: number) => val is S,
  ): AsyncIterable<S>;
}

which I could apply to a function expression, but apparently not to a function declaration per How to apply a function type to a function declaration. Is this not what the generator is?


Solution

  • So you want filterIterable() to be an overloaded function with multiple call signatures. While it's true that you can't currently directly annotate a function as requested in microsoft/TypeScript#22063, you could just use regular overload syntax and declare the call signatures before the implementation:

    // call signatures
    function filterIterable<T, S extends T>(
      iter: AsyncIterable<T>, predicate: (val: T, index: number) => val is S
    ): AsyncIterable<S>;
    function filterIterable<T>(
      iter: AsyncIterable<T>, predicate: (val: T, index: number) => boolean
    ): AsyncIterable<T>;
    
    // implementation
    async function* filterIterable<T>(
      iter: AsyncIterable<T>, predicate: (val: T, index: number) => boolean
    ) {
      let i = 0
      for await (const val of iter) {
        if (predicate(val, i++)) {
          yield val
        }
      }
    }
    

    This should now work as you intended:

    async function foo() {
      const it1 = filterIterable(gen1(), n => n % 2 === 0)
      for await (const x of it1) {
        //             ^? const x: 1 | 2 | 3
        console.log(x.toFixed(2))
      }
      const it2 = filterIterable(gen2(), nonNullable) 
      for await (const y of it2) {
        //             ^? const y: 1 | 3
        console.log(y.toFixed(3))    
      }
    }
    

    Playground link to code