Home > OS >  TypeScript type inference in a higher order function based on input of returned function
TypeScript type inference in a higher order function based on input of returned function

Time:11-03

I'm trying to create a generic "pipe" function that accepts a series of map functions that transform an input and eventually returns the output of the final map function. I started with a basic function that only accepts one map function as a parameter:

const pipe = <T, U>(map: (input: T) => U) => (initial: T): U => map(initial);

However, when I try and use it with an identity function, I get unknown back:

// test is unknown
const test = pipe(i => i)(1);

Ideally, test should be number in this example.

My hypothesis here is that pipe(i => i) is evaluated as pipe(unknown => unknown), and this doesn't get updated with any inference from the returned function. When I call pipe(unknown => unknown)(1), it's fine passing a number into a function that accepts unknown, but because that function also returns unknown, that's what eventually gets returned.

I'm wondering if my hypothesis here is correct, and if so, whether there's any activity regarding it somewhere in the TypeScript dev scene.

Is there any way in TypeScript currently to achieve what I'm looking for?

CodePudding user response:

Your indentity function tries to infer type from caller, and your pipe function tries to infer type from callback, making the type unknown.

You should define type on either pipe or i=>i, or make i=>i generic

const pipe = <T, U>(map: (input: T) => U) => (initial: T): U => map(initial);

const test0 = pipe(<T, >(i: T) => i)(0) // 0
const test1 = pipe<number, number>(i => i)(0) // number
const test2 = pipe((i: number) => i)(0) // number

CodePudding user response:

The issue is that the function (i) => i - the input type (i) is unknown.

The fix would be to say what the input type is:

Typescript playground: link description

const test = pipe((i: number) => i)(1);

The only way to have multiple pipes, in which input is the output of the other is to create a building. Like this:

type PipeFunction<Input, Output> = (input: Input) => Output;

class PipeBuilder<FirstInput,LastInput,LastOutput> {
    constructor(private pipes: Array<(input: any) => any>) {}

    public static of<Input, Output>(fn: PipeFunction<Input, Output>): PipeBuilder<Input, Input, Output> {
        return new PipeBuilder([fn]);
    }

    public pipe<Output>(fn: PipeFunction<LastOutput, Output>): PipeBuilder<FirstInput, LastOutput, Output> {
        return new PipeBuilder([ ...this.pipes, fn ]);
    }

    public build(): PipeFunction<FirstInput, LastOutput> {
        return (firstInput: FirstInput): LastOutput => {
            let result = this.pipes[0](firstInput);
            for(let i = 1; i <= this.pipes.length; i  ) {
                result = this.pipes[i](result);
            }
            return result as any as LastOutput;
        } 
    }
}

Then we can use and chain the pipes like this:

Full demo with builder here

const pipeBuilder = PipeBuilder
    .of((num: number) => num.toString())
    .pipe((str) => parseInt(str))
    .pipe(num => new Date(2022,1,1, num))
    .pipe(date => "The date is: "    date.toISOString());


const materializedFunction = pipeBuilder.build();
const result = materializedFunction(5);
console.log(result);
  • Related