Home > Mobile >  Constraining a function argument type in TypeScript for creating a pipeline that returns a goal func
Constraining a function argument type in TypeScript for creating a pipeline that returns a goal func

Time:12-31

I'm building a function pipeline that creates a series of checks/guards and then accepts a function and returns a function that either returns early or calls the accepted function. Something like this:

// A generic function type. The goal is to have the pipeline work with any type of function
interface GoalFunctionType { (): string }

// Pipeline instantiation and usage `checkSomething` and `provideName` are example functions in the pipeline, they are not implemented here.:
const p = new FunctionPipeline<GoalFunctionType>()
const passedInFunction = ({ name }: { name: string }) => "Hello "   name
const goalFunction: GoalFunctionType = p.checkSomething().provideName().finally(passedInFunction);

The pipeline checks along the way can trigger an early return in finally. They could optionally create additional arguments for function passed into finally as in provideName above (but implementations below haven't gotten that far yet).

I'm getting stuck by the type checker on the finally function. I want the type checker to ensure that the function passed in

  • has the same return type as the GoalFunctionType
  • accepts the same arguments as GoalFunctionType
  • accepts additional, pipeline-generated arguments as named arguments in the first argument (not implemented here)

Here's a minimal implementation (CodeSandbox) that isn't compiling without errors/warnings:

class FunctionPipeline<FunctionType extends (...args: any[]) => any> {
  finally(
    fn: (...args: Parameters<FunctionType>) => ReturnType<FunctionType>
  ): FunctionType {
    return (...args) => {
      return fn(...args);
    };
  }
}

interface LoaderFunction {
  ({ name }: { name: string }): string;
}

const goalFunction = new FunctionPipeline<LoaderFunction>().finally(
  ({ name }) => {
    const result = `Hello ${name}`;
    console.log(result);
    return result;
  }
);

const app = document.getElementById("app");
if (app) app.innerHTML = goalFunction({ name: "World" });

To implement pipeline-generated arguments, the finally function would be more like this and hopefully with specific type:

fn: (pipelineArgs: GeneratedArgsType, ...args: Parameters<FunctionType>) => ReturnType<FunctionType>
  ): FunctionType {
  return (...args) => {
    // example: this.generatedArgs == { name: "Nathan" };
    return fn(this.generatedArgs, ...args);
  };
}

There are two compiler errors with the functionPipeline.finally method.

Error on first return:

Type '(...args: any[]) => ReturnType<FunctionType>' is not assignable to type 'FunctionType'.
  '(...args: any[]) => ReturnType<FunctionType>' is assignable to the constraint of type 'FunctionType', but 'FunctionType' could be instantiated with a different subtype of constraint '(...args: any[]) => any'.

Error on second return:

(parameter) args: any[]
Argument of type 'any[]' is not assignable to parameter of type 'Parameters<FunctionType>'.

Can you help me figure out the right types here to accomplish my goals above? Here's the minimal example in CodeSandbox. If you want to see more code, check out this longer example that provides more application context and usage in the Remix framework.

CodePudding user response:

Your version is producing an error because there is no guarantee that FunctionPipeline<F> can produce a value of generic type F when you call finally(). F can be any subtype of any function type, including a function with extra properties on it:

function foo(x: string) {
    return x.length;
}
foo.strProp = "hey";
const gf = new FunctionPipeline<typeof foo>;
gf.finally(foo).strProp.toUpperCase() // no compiler error, but:
//            
  • Related