0

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?

2
  • 1
    Are you asking how to write the compose() function in TypeScript? Since TypeScript compiles to JavaScript, you need to be able to write an untyped compose() in JavaScript. How would you propose, at runtime, to determine which overload to use? Commented Jan 3, 2019 at 19:48
  • Also, while object property shorthand and object destructuring assignment are neat, it's hard to understand what's going on in this question. As written, the code doesn't compile (even if compose() were given to us). Are a, b, and c supposed to be in-scope variables? In operationA() etc, are the types of a etc., supposed to be any? Commented Jan 3, 2019 at 19:50

2 Answers 2

2

(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!

Sign up to request clarification or add additional context in comments.

Comments

0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.