I have 2 composing functions: compose and composeR. They receive 2 functions and compose them together in a way that, output of one function is passed as an input to another function. This is their definitions:
type Compose = <A, B, C>(
f: (x: B) => C,
g: (x: A) => B
) => (x: A) => C
const compose: Compose =
(f, g) => x => f(g(x))
type ComposeR = <A, B, C>(
g: (x: A) => B
f: (x: B) => C,
) => (x: A) => C
const composeR: ComposeR =
(g, f) => x => g(f(x))
Now trying to compose functions like below, everything compiles and all the types are inferred correctly:
type Increment = (x: number) => number
const increment: Increment = x => x+1
type ToString = (x: number) => string
const toString: ToString => x => `${x}`
const composed = compose(toString, increment)
const composedR = compose(increment, toString)
composed(12) // "13"
composedR(12) // "13"
But if I try more complex input functions, I get into compile issue for compose() function:
type Option<A> = Some<A> | None
interface Some<A> {
_tag: 'Some'
value: A
}
interface None {
_tag: 'None'
}
const some = <A,>(x: A): Option<A> =>
({ _tag: 'Some', value: x })
const none: Option<never> =
{ _tag: 'None' }
const isNone = <A,>(x: Option<A>): x is None =>
x._tag === 'None'
// --------------------
type Either<E, A> = Left<E> | Right<A>
interface Left<E> {
_tag: 'Left'
left: E
}
interface Right<A> {
_tag: 'Right'
right: A
}
const left = <E,A=never>(x: E): Either<E, A> => ({ _tag: 'Left', left: x})
const right = <A,E=never>(x: A): Either<E, A> => ({ _tag: 'Right', right: x})
const isLeft = <E, A>(a: Either<E, A>): a is Left<E> =>
a._tag === 'Left'
// --------------------
interface URItoKind1<A> {
'Option': Option<A>
}
interface URItoKind2<E,A> {
'Either': Either<E,A>
}
type URIS1 = keyof URItoKind1<any>
type URIS2 = keyof URItoKind2<any, any>
type Kind1<URI extends URIS1, A> = URItoKind1<A>[URI]
type Kind2<URI extends URIS2, E, A> = URItoKind2<E,A>[URI]
type HKT1<URI, A> = { URI: URI; a: A };
type HKT2<URI, A, B> = { URI: URI; a: A; b: B }
interface Functor1<F extends URIS1> {
readonly URI: F
map: <A, B>(f: (a: A) => B) => (fa: Kind1<F, A>) => Kind1<F, B>
}
interface Functor2<F extends URIS2> {
readonly URI: F
map: <E, A, B>(f: (a: A) => B) => (fa: Kind2<F, E, A>) => Kind2<F, E, B>
}
interface Functor<F> {
readonly URI: F
map: <A, B>(f: (a: A) => B) => (fa: HKT1<F, A>) => HKT1<F, B>
}
// --------------------------
const optionFunctor: Functor1<'Option'> = {
URI: 'Option',
map: <A,B>(f: (x: A) => B) => (fa: Option<A>): Option<B> =>
isNone(fa) ? none : some(f(fa.value))
}
const eitherFunctor: Functor2<'Either'> = {
URI: 'Either',
map: <E,A,B>(f: (x: A) => B) => (fa: Either<E, A>): Either<E, B> =>
isLeft(fa) ? fa : right(f(fa.right))
}
// ---------------------------
type Compose = <A, B, C>(
f: (x: B) => C,
g: (x: A) => B
) => (x: A) => C
const compose: Compose =
(f, g) => x => f(g(x))
type ComposeR = <A, B, C>(
g: (x: A) => B,
f: (x: B) => C
) => (x: A) => C
const composeR: ComposeR =
(g, f) => x => f(g(x))
// ---------------------------
type Increment = (x: number) => number
const increment: Increment = x => x+1
type ToStringg = (x: number) => string
const toStringg: ToStringg = x => `${x}`
const composed = compose(toStringg, increment)
const composedR = composeR(increment, toStringg)
composed(12) // "13"
composedR(12) // "13"
// This section compiles ok and types inferred correctly when composing functions.
// ---------------------------
const map1 = optionFunctor.map
const map2 = eitherFunctor.map
const composed1 = compose(map1, map2) // <=== map2 has error and types cannot be inferred correctly
const composed2 = composeR(map1, map2) // <=== map2 is ok here!
// Try switching map1 and map2. Why in `composed1` TypeScript cannot infer types correctly? how can I fix it?
What happens is, compose cannot infer types correctly. But if I try this with composeR() types are inferred with no issues. Why is the compose() function can't infer types correctly? How can I fix this?
This is considered a design limitation of TypeScript. See microsoft/TypeScript#31738.
TypeScript has very limited support for the sort of higher-order generic function propagation you're trying to achieve here. This support, as released in TypeScript 3.4 and implemented in microsoft/TypeScript#30215, only works in very specific circumstances, and your
Composetype isn't supported.According to the description of microsoft/TypeScript#30215, the higher-order function inference
In your
Compose*types, you want TypeScript to synthesize a generic function (the one fromAtoC) by using the generic type parameters from theginput (the one fromAtoB) and propagating to thefinput (the one fromBtoC).Your
ComposeRtype isand you can see that the required flow from
gtofhappens in the same order as the parameters. Left to right. So it works. But inCompose,the required flow from
gtofwould have to happen in the opposite order. Right to left. So it does not work.So that's why it's happening. It can't be fixed easily. Until and unless some more full-featured generic support is added to TypeScript (possibly involving higher-kinded types as discussed in microsoft/TypeScript#1213), you'll have to either give up or work around it.