Home > database >  Typesafe higher order function mimicing the interface of any passed in function
Typesafe higher order function mimicing the interface of any passed in function

Time:12-03

I'm trying to type a higher order function. The basic idea is that high is passed a function fn, and it returns a function that takes the exact same parameters and gives the same return type.

It's an exercise in trying to understand the language better. I have something that works but erases the type from the input parameters. Please see the types Test1 vs Test2 below:

export function high<R>(fn: (...args: any[]) => R) {
  return (...args: any[]) => {
    const moddedArgs = args.map((el) =>
      typeof el === "string" ? el   "OMG" : el
    );

    return fn(...moddedArgs);
  };
}

const test1 = (nr1: number, str1?: string) => (str1 ?? "Wow").repeat(nr1);

test1(3, "yo");
// returns yoyoyo

test1(3);
// returns WowWowWow

const test2 = high(test1);

test2(3, "yo");
// returns yoOMGyoOMGyoOMG

type Test1 = typeof test1;
// type Test1 = (nr1: number, str1?: string) => string

type Test2 = typeof test2;
// type Test2 = (...args: any[]) => string

Playground Link

I tried having a P type argument for the parameters, which I sort of got working But it didn't handle optional parameters well.

Any ideas how this can be solved?

CodePudding user response:

You should store the whole type of fn in a generic type. To type ...args, we can use Parameters<Fn>.

export function high<Fn extends (...args: any[]) => any>(fn: Fn) {
  return (...args: Parameters<Fn>) => {
    const moddedArgs = args.map((el) =>
      typeof el === "string" ? el   "OMG" : el
    );

    return fn(...moddedArgs);
  };
}

Playground

CodePudding user response:

Instead of trying to guess generics for the fn function parameters and return types separately, we can have a single generic type F for the whole function, and then extract its constituent types with Parameters and ReturnType TypeScript built-in utility types:

export function high<F extends (...args: any[]) => any>(fn: F) {
  return (...args: Parameters<F>): ReturnType<F> => {
    const moddedArgs = args.map((el) =>
      typeof el === "string" ? el   "OMG" : el
    );

    return fn(...moddedArgs);
  };
}

const test2 = high(test1);
//    ^? (nr1: number, str1?: string | undefined) => string

We can even go a little further, in case it might make sense in your case: by asserting the modified function as having exactly the same type as the original function (type F above), we get its associated JSDoc as well!

function high2<F extends (...args: any[]) => any>(fn: F) {
  return high(fn) as F // If we assert the returned function type as exactly F, we get its associated JSDoc as well!
}

const test22 = high2(test1);
//    ^? (nr1: number, str1?: string) => string
// In the IDE, we should have any JSDoc associated to test1, popup as well on test22, since it is said to be exactly the same type.

Playground Link

  • Related