Typescript tapAsync function unable to set correct typings

Viewed 140

I want to be able to tap a few ES6 async functions together using typescript. The tap function should return the argument if nothing is returned from the tapped function, but tap the returned value if anything is returned from a tapped function.

typescript playground example

I can get it to work without typeing, but having issues setting the types. Please see the snippet for an example of the code working in Javascript.

The tapped function is simply called with the x value x => fn(x), then chained to return either the return value y or the tapped value x x => fn(x).then(y => y || x)

This first version using the any type works, but I'm getting errors when getting specific on handling the types in the tapped functions.

const tapAsync = (fn: (x: any) => Promise<any>) => (
  x: any
): Promise<any> => fn(x).then((y: any) => y || x)

To get more specific I'm using Two generics, X for the initial argument, and Y for the retured value of the tapped function.

const tapAsync = (fn: <X>(x: X) => Promise<X>) => (
  x: X
): Promise<Y|X> => fn(x).then(<Y>(y: Y) => y || x)

When I call the functions using tapAsync I get the following error.

src/index.ts:45:18 - error TS2345: Argument of type '({ foo }: { foo: any; }) => Promise<void>' is not assignable to parameter of type '<X>(x: X) => Promise<X>'.
  Types of parameters '__0' and 'x' are incompatible.
    Type 'X' is not assignable to type '{ foo: any; }'.

45   .then(tapAsync(one))
                    ~~~
src/index.ts:46:18 - error TS2345: Argument of type '({ foo }: { foo: any; }) => Promise<{ foo: any; bar: string; }>' is not assignable to parameter of type '<X>(x: X) => Promise<X>'.
  Types of parameters '__0' and 'x' are incompatible.
    Type 'X' is not assignable to type '{ foo: any; }'.

46   .then(tapAsync(two))
                    ~~~
src/index.ts:47:18 - error TS2345: Argument of type '({ foo, bar }: { foo: any; bar: any; }) => Promise<void>' is not assignable to parameter of type '<X>(x: X) => Promise<X>'.
  Types of parameters '__0' and 'x' are incompatible.
    Type 'X' is not assignable to type '{ foo: any; bar: any; }'.

47   .then(tapAsync(three))

I'm not setting any types in typescript on the tapped functions, but I have tried using generic types on the second function two but that also doesn't work

async function two<X>({ foo }): Promise<X> {
  console.log('two', foo)
  return {
    foo,
    bar: 'bar'
  }
}

async function one({ foo }) {
  console.log('one', foo)
}

async function two({ foo }) {
  console.log('two', foo)
  return {
    foo,
    bar: 'bar'
  }
}

async function three({ foo, bar }) {
  console.log('three', foo, bar)
}

const tapAsync = fn => x => fn(x).then(y => y || x)

Promise.resolve({ foo: 'foo' })
  .then(tapAsync(one))
  .then(tapAsync(two))
  .then(tapAsync(three))

Thanks for any help!

============== edit 2020-09-01 ====================

I have been playing with the code and have fleshed out the types a little more, but now I get an error on the two function when it returns a new object even though it is the same shape.

new typescript playground example

const tapAsync = <X, Y>(fn: (x: X) => Promise<Y|void>) => 
  (x: X): Promise<X|Y> => 
    fn(x).then((y: Y|void) => y || x)
1 Answers

I think with overloads it works. And type inferencing looks correct. Basically this allows the ts compiler to infer that there never is a (x:X)=>Promise<void | Y>as return. Which happened, because after the 1st call to a non returning async function :Promise<void> Y was infered to be of that type, and so the Promise.then(... would try to feed X as a void | Args in the next call, resulting in a (x: void | Args)=>.... which is incompatible with (x: Args)=>... expected.

Check this playground your example with overloads

function tapAsync <X,Y>(fn: (x:X)=>Promise<void>): (x:X)=>Promise<X>;
function tapAsync <X,Y>(fn: (x:X)=>Promise<Y>): (x:X)=>Promise<Y>;
function tapAsync <X, Y>(fn: (x: X) => Promise<Y|void>) {
  return (x: X) => fn(x).then(y => y||x )
}

Edit: Rereading my answer, I failed to mention, the culprit is the (y: void| Y )=> Y||X not being able to inference that it can never return a void if the initial X was not a void.

Related