简体   繁体   中英

Typescript function overloading based on input arguments

I'm looking for a way to override or overload a specific group of functions.

Here's an example

const operationA = ({a, b, c}) => 'alpha';
const operationB = ({a, b }) => 'beta';
const operationC = ({a}) => 'gamma';

const operation = compose([operationA, operationB, operationC]);

operation({a}) // 'gamma'
operation({a, b}) // 'beta'
operation({a, b, c}) // 'alpha'

Is there a way to do this functionality within typescript?

(In the following I'm using TypeScript 3.2)

The main issue with your question, if I understand it, is the difficulty of choosing the right overload at runtime. It is not one of TypeScript's goals (see Non-Goal #5) to compile type information from TypeScript into JavaScript. The type system added by TypeScript is completely erased at runtime. So, if you want to write compose() to take a list of functions, somehow you have to be able to inspect those functions at runtime to determine which one should be called on a particular argument. That functionality really doesn't exist in JavaScript, though. Well, you can kind of use the length property of a function to see how many arguments it expects, but in the examples you gave, each function takes exactly one argument. So we can't use that approach here.

One possible way forward is to add a property to each function. This property would be a method that takes a potential set of arguments and returns true if those arguments are valid for the function, and false if they are not. Essentially you're manually adding the necessary inspection ability that is missing from the language.

If we do this, we can make compose() accept a list of such "argument validating functions", like this:

type ArgValidatingFunction =
  ((...args: any[]) => any) & { validArgs(...args: any): boolean };

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends
  ((k: infer I) => void) ? I : never;

function compose<F extends ArgValidatingFunction[]>(...fn: F): UnionToIntersection<F[number]>;
function compose(...fn: ArgValidatingFunction[]): Function {
  return Object.assign(
    (...args: any[]) => (fn.find(f => f.validArgs(...args))!(...args)),
    { validArgs: (...args: any[]) => fn.some(f => f.validArgs(...args)) }
  );
}

The type signature for compose accepts a list of ArgValidatingFunction arguments and returns the intersection of its elements . TypeScript represents overloads as an order-dependent intersection of signatures. I can't 100% guarantee that the compiler will produce the same overload order as the functions passed in, but it seems to work in my testing.

The implementation of compose makes use of the ArgValidatingFunction 's validArgs method, and does a find() on the passed-in functions to choose the proper function. I also implement a validArgs() method on the returned function so that the return value of compose() is also an ArgValidatingFunction (which is good because the type signature claims that it is).

Now we can try to use it, but it's not trivial... we have to add those methods:

const operationA = ({ a, b, c }: { a: any, b: any, c: any }): 'alpha' => 'alpha';
operationA.validArgs = (...args: any[]) => 
  (args.length === 1) && ('a' in args[0]) && ('b' in args[0]) && ('c' in args[0]);

const operationB = ({ a, b }: { a: any, b: any }): 'beta' => 'beta';
operationB.validArgs = (...args: any[]) => 
  (args.length === 1) && ('a' in args[0]) && ('b' in args[0]);

const operationC = ({ a }: { a: any }): 'gamma' => 'gamma';
operationC.validArgs = (...args: any[]) => 
  (args.length === 1) && ('a' in args[0]);

Here we go:

const operation = compose(operationA, operationB, operationC);

const beta = operation({ a: 3, b: 3 }); // "beta" at compile time;
console.log(beta); // "beta" at runtime

Looks like it works both at compile time and runtime.


So that's one way to go. It's not easy or pretty, but maybe it works for your (or someone's) use case. Hope that helps. Good luck!

An approach, which maybe you have already evaluated is to use an Interface as input of your main operation method, and then dispatch the right sub methods depending on the input.

So something like:

interface OperationArgs {
 a: string;
 b?: string;
 c?: string;
}

So the first value is mandatory, the other two are optionals.

Inside your operation method you can do something like:

public operation(inp: OperationArgs) {
  if (inp.c) {
    return this.operationC(inp);
  }
  if (inp.b) {
    return this.operationB(inp);
  }
  return this.operationA(inp);
}

Another approach is to use Proxy but they are not fully supported yet in JS (explorer is missing). You could create a class which returns a Proxy instance, and trap the operation methods using the get method of the handler. Depending on the given props, you will actually call the right method on the instance.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM