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?
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 typesevalfn
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With