Typescript doesn't figure out the generic

Viewed 102

I'm starting to learn about typescript and I tried to make an parser combinator library, at first types and generics where easy, but an issue appeared.

// The generic T represents the value the parser returns
class Parser<T> {
    ...
}

// This function combines the two parsers in one tuple
function pair<P1 extends Parser<A>, P2 extends Parser<B>, A, B>(
  parser1: Parser<A>,
  parser2: Parser<B>
): Parser<{ fst: A, snd: B }> {
    ...
}

// Maps a Parser<A> to a Parser<B> through A => B
function map<P extends Parser<A>, F extends (a: A) => B, A, B>(
  parser: P, mapFn: F 
): Parser<B> {
    ...
}

Here is where I have the issue:

// Like pair but the value of the parser is A, instead of {fst: A, snd: B}
function left<P1 extends Parser<A>, P2 extends Parser<B>, A, B>(
  parser1: P1,
  parser2: P2
): Parser<A> {
  return map(pair(parser1, parser2), (a: unknown): A => (a as {fst: R1, snd: R2}).fs );
}

The provided left function compiles but I don't understand why typescript cannot infer that the unknown should be {fst: A, snd: B}, is this a typescript issue or just me mistaking how typescript generics work.
Thanks in advance.

EDIT
Added reproducible playground:
Here is the link reproducible

2 Answers

All you need to do is to explicitly infer Parser generic and map callback return type.

Example:

// The generic T represents the value the parser returns
class Parser<T> { }

// This function combines the two parsers in one tuple
function pair<P1 extends Parser<A>, P2 extends Parser<B>, A, B>(
  parser1: Parser<A>,
  parser2: Parser<B>
): Parser<{ fst: A, snd: B }> {
  return null as any
}

type InferGeneric<T extends Parser<unknown>> = T extends Parser<infer R> ? R : never

function map<P extends Parser<A>, F extends (a: /** explicit infer generic */ InferGeneric<P>) => B, A, B>(
  parser: P, mapFn: F
): Parser<ReturnType<F>> /** Explicit ReturnType infer */ {
  return null as any
}


// Like pair but the value of the parser is A, instead of {fst: A, snd: B}
function left<P1 extends Parser<A>, P2 extends Parser<B>, A, B>(
  parser1: P1,
  parser2: P2
): Parser<A> {
  return map(pair(parser1, parser2), a => 42); // ok
}

Playground

Use Fewer Generics

When you have generic type parameters that are being inferred as unknown, one of the best things that you can do is to reduce the number of generics type parameters on your function and instead derive certain types from the others.

Sometimes this involves using built-in utility types like ReturnType<T> to get the return type from the type of a function T. We can also define our own utility types by using a conditional along with infer/extends.

In your example there is actually a very simple solution where A and B are the only generic values.

class Wrapper<T> {
    callable: () => T;
    constructor(callable: () => T) {
        this.callable = callable;
    }
}

function map<A, B>(w: Wrapper<A>, mapFn: (a: A) => B): Wrapper<B> {
    return new Wrapper(() => mapFn(w.callable()));
}

function pair<A, B>(w1: Wrapper<A>, w2: Wrapper<B>): Wrapper<{fst: A, snd: B}> {
    return new Wrapper(() => ({fst: w1.callable(), snd: w2.callable()}));
}

function left<A, B>(w1: Wrapper<A>, w2: Wrapper<B>): Wrapper<A> {
    return map(pair(w1, w2), value => value.fst);
}

Typescript Playground Link

Related