Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Abstracting away a generic type parameter in typescript

Tags:

typescript

I'm running into a situation where I can't seem to avoid any in Typescript. Here's an example that mirrors what I'm trying to do:

type NativeFn<A, B> = {
    kind: 'native'
    fn: (a: A) => B
}

type ComposeFn<A, B, C> = {
    kind: 'compose'
    f: Fn<B, C>
    g: Fn<A, B>
}

type Fn<A, B> = NativeFn<A, B>
    | ComposeFn<A, any, B>  // <=== HERE

function evalfn<A, B>(fn: Fn<A, B>, arg: A): B {
    switch (fn.kind) {
        case 'native': return fn.fn(arg)
        case 'compose': {
            // intermediate has type "any", which is a drag.
            const intermediate = evalfn(fn.g, arg)
            return evalfn(fn.f, intermediate)
        }
    }
}

What I want to say is that ComposeFn<A, B, C> is always a Fn<A, C>, no matter which type B is, but B should still be typed.

With any, I can incorrectly type things like:

const F: Fn<string, string[]> = { kind: 'native', fn: (n) => [n] }

const G: Fn<number, number> = { kind: 'native', fn: (n) => n + 1 }

const FoG: Fn<number, string[]> = {
    kind: 'compose',
    f: F,
    g: G,
}

unknown doesn't work either. Example.

Is there any way do accomplish what I'm going for here?

like image 497
hjfreyer Avatar asked Oct 16 '22 05:10

hjfreyer


1 Answers

I would suggest simple thing, as you have union which has different arity of generics NativeFn has two arguments and ComposeFn has three, then the main one should also have three in order to not have a blank space, we can achieve that by default value of the third. Consider:

type Fn<A, B, C = B> = NativeFn<A, C>
  | ComposeFn<A, B, C>

function evalfn<A, B, C = B>(fn: Fn<A, B, C>, arg: A): C {
    switch (fn.kind) {
        case 'native': return fn.fn(arg)
        case 'compose': {
            const intermediate = evalfn(fn.g, arg)
            return evalfn(fn.f, intermediate)
        }
    }
}

What we did here:

  • Fn has always three generic arguments. With default C=B
  • evalfn also works with three argument generic types
  • we change the return of evalfn to C

Lets check if it works correctly:

// my utility to make such construct
const makeNativeF = <A, B>(fn: (a: A) => B): NativeFn<A,B> => ({
  kind: 'native',
  fn
})
const nativeF = makeNativeF((a: number) => a);
const resultNative = evalfn(nativeF, 1); // correct number type result!

// my utility to make such construct
const makeComposeF = <A, B, C>(f: Fn<B,C>, g: Fn<A,B>): ComposeFn<A, B, C> => ({
  kind: 'compose',
  f,
  g
});

const composeF = makeComposeF(makeNativeF((a: number) => a + ': string'), makeNativeF((a: number) => a));
const resultComposed = evalfn(composeF, 1); // result is string! correct!

Everything looks good. Hope it helps.

like image 192
Maciej Sikora Avatar answered Oct 20 '22 23:10

Maciej Sikora