Call a member of an object with arguments

Viewed 2382
function whatever(object, methodName, args) {
  return object[methodName](...args);
}

Can the above be typed so the the following is enforced:

  • methodName is a key of object.
  • object[methodName] is callable and its args are ...args.
  • The return type of whatever(object, methodName, args) is the return type of object[methodName](...args).

The closest thing I could find is the definition of function.apply, but it isn't quite the same as the above.

8 Answers

I think this does the trick:

function callMethodWithArgs<
  M extends keyof T,
  T extends { [m in M]: (...args: Array<any>) => any },
  F extends T[M]
>(obj: T, methodName: M, args: Parameters<F>) {
  return obj[methodName](...args) as ReturnType<F>;
}

Requires TS 3 though!

type Dictionary = { [key: string]: any }

type MethodNames<T extends Dictionary> = T extends ReadonlyArray<any>
  ? Exclude<keyof [], number>
  : { [P in keyof T]: T[P] extends Function ? P : never }[keyof T]

function apply<T extends Dictionary, P extends MethodNames<T>>(
  obj: T,
  methodName: P,
  args: Parameters<T[P]>
): ReturnType<T[P]> {
  return obj[methodName](...args);
}

// Testing object types:
const obj = { add: (...args: number[]) => {} }
apply(obj, 'add', [1, 2, 3, 4, 5])

// Testing array types:
apply([1, 2, 3], 'push', [4])

// Testing the return type:
let result: number = apply(new Map<number, number>(), 'get', [1])

Playground link

The Dictionary type allows T[P] to be used.

The Parameters and ReturnType types are baked into TypeScript.

The MethodNames type extracts any keys whose value is assignable to the Function type. Array types require a special case.

Checklist

  • Method name is validated? ✅
  • Arguments are type-checked? ✅
  • Return type is correct? ✅

Is the return type always the same of someObject[methodName]?

function whatever<O extends {[key: string]: (...args) => R}, R>(object: O, methodName: keyof O, ...args: any[]): R {
  return object[methodName](...args);
}

Then you could do this.

This should do it. This checks for the methodName as well as each of the args.

(Note: not perfect, some refinement can be made; e.g., unknown -> any)

type ArgumentsType<T> = T extends (...args: infer A) => any ? A : never;

function whatever<
  T extends object,
  TKey extends keyof T,
  TArgs extends ArgumentsType<T[TKey]>
>(
  object: T,
  methodName: T[TKey] extends ((...args: TArgs) => unknown) ? TKey : never,
  args: TArgs
): T[TKey] extends ((...args: TArgs) => unknown) ? ReturnType<T[TKey]> : never {
  const method = object[methodName];
  if (typeof method !== 'function') {
    throw new Error('not a function');
  }
  return method(...args);
}

interface Test {
  foo: (a: number, b: number) => number;
  bar: string;
}

const test: Test = {
  foo: (a, b) => a + b,
  bar: 'not a function'
};

const result = whatever(test, 'foo', [1, 2]);

How about this?

function whatever(someObject: { [key: string]: Function}, methodName: string, args: any[]) {
    return someObject[methodName](...args);
}

whatever({ func1: (args) => (console.log(...args)) }, 'func1', [1])

The closest result I can think of so far is the following


    function whatever<T extends object>(object: T, methodName: keyof T, args: any) {
        const func = object[methodName]
        if (typeof func === "function") {
            return func(...[args])
        }
    }

    const obj = {
      aValue: 1,
      aFunc: (s: string) => "received: " + s
    }

    whatever(obj, "aFunc", "blabla")

which correctly checks the key for being part of the object. The type inference for return type and args is still missing though. I will update the answer if I find a better solution.

The option that is the type-safest one is to use Parameters to get the parameter types of the function (in order for arguments to be type safe), ReturnType to return the result of a function.

You also need to also add a type parameter to capture the actual key passed in so we can get the actual type of the function (T[K](

function whatever<T extends Record<string, (...a: any[])=> any>, K extends keyof T> (someObject: T,  methodName: K, ...args: Parameters<T[K]>) : ReturnType<T[K]>{
    return someObject[methodName](...args);
}

whatever({ func1: (args: number[]) => (console.log(...args)) }, 'func1', [1])
whatever({ func1: (args: number[]) => (console.log(...args)) }, 'func1', ["1"]) // err

There you go, meets all criteria and doesn't need type assertions.

type AnyFunction = (...args: any[]) => any;

function whatever<
  T extends Record<PropertyKey, AnyFunction>,
  K extends keyof T,
  A extends Parameters<T[K]>
>(object: T, methodName: K, args: A): ReturnType<T[K]> {
    return object[methodName](...args);
}

The Parameters type is a part of the standard library since TypeScript 3.1. If you're using an older version, create it yourself:

type Parameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any
    ? P
    : never;

Using the PropertyKey type instead of string allows you to use properties of type string | number | symbol, which is the full gamut supported by JavaScript.

Related