I created a simple typescript compose function
One of the functions I'm composing has a conditional return type based on the input
When I'm adding this function to the compose function, the conditional return type stops working
My question is why, and if there is a way I can make it work?
Here is an example
type Fn = (arg: any) => any
export function compose<Input, A1>(
fn1: (input: Input) => A1,
): (input: Input) => A1;
export function compose<Input, A1, A2>(
fn1: (input: Input) => A1,
fn2: (input: A1) => A2,
): (input: Input) => A2;
export function compose(...args: Fn[]) {
return (data: any) => args.reduce((acc, elem) => elem(acc), data) as any
}
/////////////////////////
const a = <I, O>(fn: (args: I) => O) => (args: I & {a: number}): O & {a: number} => {
return {...fn(args as I), a: 1}
}
const b = <I, O>(fn: (args: I) => O) => <B extends boolean>(args: I & {b: B})
: B extends true ? O & {b: number} : O => {
if(!args.b) return fn(args) as any
return {...fn(args), b: 1}
}
const fn = (arg: {x: number}) => arg
compose(b, a)(fn)({
x:1,
a:1,
b:true
}).b // <- Property 'b' does not exist
CodePudding user response:
This is a known limitation in higher order generic function inference implemented in microsoft/TypeScript#30215, as described in microsoft/TypeScript#30727. Although that issue is marked as "needs investigation", the implementer of this functionality commented that it's not obvious how to improve the situation.
In order to save my sanity I'm going to rename your I
and O
type parameters to keep track of which function they came from:
const a = <Ia, Oa>(fn: (args: Ia) => Oa) =>
(args: Ia & {a: number}): Oa & {a: number} => {
return {...fn(args as Ia), a: 1}
}
const b = <Ib, Ob>(fn: (args: Ib) => Ob) =>
<B extends boolean>(args: Ib & {b: B})
: B extends true ? Ob & {b: number} : Ob => {
if(!args.b) return fn(args) as any
return {...fn(args), b: 1}
}
When you call compose(b, a)
, the compiler needs to infer A1
as a unification of the <B extends boolean>(args: Ib & {b: B}) => B extends true ? Ob & {b: number} : Ob
returned by b
and the (fn: (args: Ia) => Oa)
accepted by a
. It has to instantiate Ia
and Oa
to do this, and here is where the problem happens.
There is currently no facility to promote the "inner" type parameter B
to an outer scope; instead the compiler loses the generic and replaces B
with its constraint, boolean
. That means Ia
becomes Ib & {b: boolean}
and Oa
becomes Ob | (Ob & {b: number})
(since it was a distributive conditional type).
Everything else follows from there. The returned value is generic in Ib
and Ob
, and you get a value of type <Ib, Ob>(input: (args: Ib) => Ob) => (args: Ib & { b: boolean } & { a: number }) => (Ob | (Ob & { b: number })) & { a: number }
. Oops.
As far as I know there's no way to "make it work". TypeScript lacks true higher kinded types as requested in microsoft/TypeScript#1213. Without higher kinded types there's no general way to express what you're trying to do. The current higher order generic function support from microsoft/TypeScript#30215 is a "best-effort" heuristic that only works in particular circumstances. And unfortunately your example isn't one of those circumstances.