user6101582
user6101582

Reputation:

How should the lodash flow function be typed in typescript?

lodash.flow combines two or more functions.

lodash.flow(double, addTwo) would return a function the would double and add two. How should this function be typed in typescript (the current definition just returns Function)?

declare function flow<In, Intermediate, Out>(f1: (a1: In) => Intermediate, f2: (a1: Intermediate) => Out): (a1: In)=> Out works for two functions with the first having one input argument. I'm not sure how to extend the definition to work in all cases.

My attempt can support mulitple functions if called like this:

lodash.flow(f1, lodash.flow(f2, f3))

but am looking for

lodash.flow(f1, f2, f3)

Upvotes: 7

Views: 2516

Answers (2)

Nitsan Avni
Nitsan Avni

Reputation: 841

here's a way to kind of do it using conditional types:

const compose =
  <T1, T2>(f1: (p: T1) => T2) =>
  <T3>(f2: (p: T2) => T3) =>
  (p: T1) =>
    f2(f1(p));

const flow = <T1, T2, T3 = "♘", T4 = "♘">(
  f0: (p: T1) => T2,
  ...f: [] | [(p: T2) => T3] | [(p: T2) => T3, (p: T3) => T4]
): ((p: T1) => T3 extends "♘" ? T2 : T4 extends "♘" ? T3 : T4) => {
  if (f[1]) {
    return compose(compose(f0)(f[0]!))(f[1]) as any;
  }

  if (f[0]) {
    return compose(f0)(f[0]) as any;
  }

  return f0 as any;
};

in this example, flow supports up to 3 args, but it can be extended to as many as you like.

Upvotes: 0

Paarth
Paarth

Reputation: 10377

I don't believe you can write that definition.

If we look at the lodash type declaration file they don't try to express that relationship.

interface LoDashStatic {
    flow<TResult extends Function>(...funcs: Function[]): TResult;
}

But that alone isn't reason enough to discount the possibility. The authors may have just overlooked something, so let's keep thinking about it.

The relationship between an individual chain of functions is something you can represent. You've done so in your example above. You could create manual versions of that same idea for several lengths of parameters but that's because you're setting up a situation where the length of the chain is known and you can grant individual type information.

If we are to handle the case of variable length parameters, we must treat the parameters as a Collection. Because all variables must have a single (though possibly parameterized) type so too must this collection object. However, the types of the various functions do not align. (param:A) => B is not the same type as (param:B) => C and cannot be stored in the same well typed container (barring union types but those won't scale either).

In situations where you want to retain type information on a list of parameters like this you usually define the composition function on two parameters and apply it across a number of functions. This is how type information is retained in promises, for example. For this to work you still need to have each individual parameter spelled out. It just makes it so by the end you've got the proper output type. That said, in most cases this is what you want so it's all good.

If lodash were written in a well typed functional language, that flow function probably wouldn't exist. I imagine it would instead have been written as a piped composition object.

UPDATE: What do I mean when I say a "piped composition object"? Something like this, perhaps:

class FunctionComposer<T,V> {
    constructor(protected func: (param: T) => V) { }

    public compose<X>(newFunc: (param:V) => X) {
        return new FunctionComposer((x: T) => newFunc(this.func(x)));
    }
    public out() {
        return this.func;
    }
}

let composedFunc = new FunctionComposer((x: number) => x * 2)
    .compose(x => x.toString())
    .out();

// composedFunc has type (param:number) => string

Upvotes: 2

Related Questions