bigbeno37
bigbeno37

Reputation: 5

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

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?

Upvotes: 0

Views: 619

Answers (2)

Svetoslav Petkov
Svetoslav Petkov

Reputation: 1575

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);

Upvotes: 0

Dimava
Dimava

Reputation: 10899

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

Upvotes: 2

Related Questions