spacefluff432
spacefluff432

Reputation: 636

Is it possible to use the return type of a function within that function's argument types?

For example, when I try to do this:

function chain<A, B extends (input: A, loop: (input: A) => C) => any, C extends ReturnType<B>> (input: A, handler: B): C {
   const loop = (input: A): C => handler(input, loop);
   return loop(input);
}

chain('test', async (value, loop): Promise<string> => {
   const ready = await someExternalThing();
   if (ready) {
      return value;
   } else {
      return await loop(value + '!');
   }
});

Even with a strict return type defined for the handler function, the loop function's return type in the bottom code is still showing up as "any"

Does anybody know any way around this issue?

Upvotes: 1

Views: 88

Answers (3)

Finomnis
Finomnis

Reputation: 22848

I wasn't able to derive the template parameters automatically from the return type of the handler. Still searching for a solution there.

But if you are willed to give the template parameters manually, there is a solution for you:

let i = 0;

async function someExternalThing() {
    i += 1;
    return i > 3;
}

async function chain<VAL, RET>(
    input: VAL,
    handler: (input: VAL, loop: (input: VAL) => Promise<RET>) => Promise<RET>
): Promise<RET> {
    const loop = (input: VAL): Promise<RET> => handler(input, loop);
    return loop(input);
}

const result = chain<string, string>('test', async (value, loop) => {
    const ready = await someExternalThing();
    if (ready) {
        return value;
    } else {
        return await loop(value + '!');
    }
});

result.then((value) => {
    console.log(`Result: ${value}`)
})
Result: test!!!

And not a single type in the script is any or unknown.


Further research

This one seems to work:

async function chain<VAL, RET>(
    input: VAL,
    handler: (input: VAL, loop: (input: VAL) => Promise<RET>) => Promise<RET>
): Promise<RET> {
    const loop = (input: VAL): Promise<RET> => handler(input, loop);
    return loop(input);
}

const result = chain('test', async (value): Promise<string> => {
    return value;
});

But as soon as you introduce the loop parameter, RET goes back to unknown. So I feel like it's a limitation of the typescript type inferring mechanism at the moment. I think it might work in future.

async function chain<VAL, RET>(
    input: VAL,
    handler: (input: VAL, loop: (input: VAL) => Promise<RET>) => Promise<RET>
): Promise<RET> {
    const loop = (input: VAL): Promise<RET> => handler(input, loop);
    return loop(input);
}

const result = chain('test', async (value, loop): Promise<string> => {
    return value;
});

Upvotes: 1

spacefluff432
spacefluff432

Reputation: 636

Based on the solutions of Gael J and Finomnis, I decided to make a version that only requires specifying one type parameter, rather than two:

class Chainer<A> {
   run <B>(input: B, handler: (input: B, loop: (input: B) => A) => A) {
      const loop = (input: B) => handler(input, loop);
      return loop(input);
   };
}

new Chainer<Promise<string>>().run('test', async (value, loop) => {
   const ready = await someExternalThing();
   if (ready) {
      return value;
   } else {
      return await loop(value + '!');
   }
});

It uses a class as a sort of holder for the handler return type, then uses the run method to inference the "input" type from the first argument.

Another Method

Using a wrapper function instead of a class:

function chain<A> () {
   return <B>(input: B, handler: (input: B, loop: (input: B) => A) => A) => {
      const loop = (input: B) => handler(input, loop);
      return loop(input);
   };
}

chain<Promise<string>>()('test', async (value, loop) => {
   const ready = await someExternalThing();
   if (ready) {
      return value;
   } else {
      return await loop(value + '!');
   }
});

Upvotes: 1

Ga&#235;l J
Ga&#235;l J

Reputation: 15305

The following works and do what you want as far as I understand:

function chain<A, C> (input: A, handler: (input: A, loop: (input: A) => C) => C): C {
   const loop = (input: A): C => handler(input, loop);
   return loop(input);
}

chain<string, Promise<string>>('test', async (value, loop) => {
   const ready = await someExternalThing();
   if (ready) {
      return value;
   } else {
      return await loop(value + '!');
   }
})

The key being to specify the generic parameters when calling chain.

Upvotes: 1

Related Questions