简体   繁体   中英

Typescript dynamic function by key/value of object

Is there a way to set up a function based on an initial interface? I have a bunch of listOfFunctions and a an equal amount of interfaces that contain types of the functions. I would like to create a factory that returns a custom hook. The idea is to save a ton of code and make editing and adding features to all the hooks a breeze.

FunctionInterface Is all of the function names and their returns.

listOfFunctions is a setup function (the real code has a config) that returns a object with a list of functions.

theChosenFunction is a function that will be returned in the hook that allows me to interact with one of the listOfFunction functions. This means parameters (if needed). The functions in the real app are Promises that will set state of the hook. In this example, I just return a value and setState.

A) Is this possible to do with funcObject[funcName]() and create a function to invoke with typescript? This would need that ability to add params or not.

B) Is there a way to get the return value of a function type? So in this example: type CanThisWork = () => string; I want to extract string ; The idea is not to do a refactor on all of the listOfFunctions and FunctionInterfaces. There are a lot of them. If I have to create one complicated Factory, then I find this more economical.

interface FunctionInterface {
  noParamsNoReturn: () => void;
  noParamsNumberReturn: () => number;
  propsAndNoReturn: (id: string) => void;
  propsAndReturn: (id: string) => string;
}

const listOfFunctions = (): FunctionInterface => {
  return {
    noParamsNoReturn: () => void,
    noParamsNumberReturn: () => 1,
    propsAndNoReturn: (id: string) => console.log(id),
    propsAndReturn: (id: string) => id,
  }
}

function runThis<T>(funcName: keyof T, funcObject: T): void {
  const [value, setValue] = React.useState();

  // Can I have typescript set up the params here?
  const theChosenFunction = ();
  const theChosenFunction = (props) => {
    // Can I have typescript invoke the function properly here?
    const result = funcObject[funcName](); // funcObject[funcName](props)
    if (result) {
       setValue(result);
    }
  }
  return {
    theChosenFunction
  }
}


const result = runThis<FunctionInterface>('noParamsNoReturn', listOfFunctions());

result.theChosenFunction()
// OR
result.theChosenFunction('someId');

There are built-in Parameters<T> and ReturnType<T> utility types that take a function type T and use conditional type inference to extract the tuple of parameters and the return type, respectively:

type SomeFuncType = (x: string, y: number) => boolean;
type SomeFuncParams = Parameters<SomeFuncType>; // [x: string, y: number]
type SomeFuncRet = ReturnType<SomeFuncType>; // boolean

As for your runThis() function, I'd be inclined to give it the following typings and implementation, assuming you want the minimal changes that compile and run:

function runThis<K extends PropertyKey, F extends Record<K, (...args: any[]) => any>>(
  funcName: K, funcObject: F
) {
  const [value, setValue] = React.useState();
  const theChosenFunction = (...props: Parameters<F[K]>): void => {
    const result = funcObject[funcName](...props);
    if (result) {
      setValue(result);
    }
  }
  return {
    theChosenFunction
  }
}

I've given the function two generic type parameters: K , corresponding to the type of funcName , and F , corresponding to the type of funcObject . The constraints K extends PropertyKey and F extends Record<K, (...args: any[])=>any> guarantee that funcName will be of a key-like type ( string , number , or symbol ), and that funcObject will have a function-valued property at that key.

Then, we can make theChosenFunction use spread and rest syntax to allow the function to be called with a variadic list of parameters. So (...props) => funcObject[funcName](...props) will accept any number of parameters (including zero) and pass them to the called function. TypeScript represents such lists-of-parameters as a tuple type. Therefore, having theChosenFunction 's call signature look like (...props: Parameters<F[K]>) => void means that it will accept the same parameters as the entry in funcObject at the key funcName , and that it will not output anything (because your implementation doesn't output anything).


Let's see if it works:

const result = runThis('noParamsNoReturn', listOfFunctions());
result.theChosenFunction(); // okay
result.theChosenFunction('someId'); // error! Expected 0 arguments, but got 1.

const anotherResult = runThis('propsAndReturn', listOfFunctions());
anotherResult.theChosenFunction(); // error! Expected 1 arguments, but got 0.
anotherResult.theChosenFunction("someId"); // okay

runThis(listOfFunctions(), 'someId'); // error! '
// FunctionInterface' is not assignable to 'string | number | symbol'.
runThis('foo', listOfFunctions()); // error!  
// Property 'foo' is missing in type 'FunctionInterface'

runThis(
  'bar', 
  { bar: (x: string, y: number, z: boolean) => { } }
).theChosenFunction("hey", 123, true); // okay

Looks good to me.

Playground link to code

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