Search code examples
typescripttype-inferencetypescript-generics

Typescript - wrapping functions while preserving signatures


I'm trying to figure out how to wrap defined functions so I can do additional work while preserving their signatures. Here's the desired effect:

Programmer defines interface:

const actions = {
    first: (id: number) => {/*...*/},
    second: (name: string) => {/*...*/}
}
let actionsInterface = wrap(actions)
export actionsInterface

actionsInterface should (i.e. that's the goal) have the following interface:

{
    first: (id: number) => void,
    second: (name: string) => void
}

It basically provides the same exact interface (i.e. same list of functions, with same parameters, not counting the return type) as it was first defined, but there is additional work that is being done, that was injected by the wrap().

My current implementation is something like:

type VarFn = (...args: any) => any

function wrap<T, K extends keyof T>
(funcList: Record<K, T[K] extends VarFn ? T[K] : never>) {

    // this maps a specific function to a function that does something extra
    function wrapOne<T extends (...args: any)=>any>(fn: T) {
        return (...args: Parameters<typeof fn>) => {
            someMagicThingyExtra(fn(args))
        }
    }

    // we iterate through the list and map each function to the one that's doing something extra
    type FuncMap = Record<K, (...args: Parameters<T[K] extends VarFn ? T[K] : never>)=>void>
    let map: FuncMap
    for (var Key in funcList) {
        let func = funcList[Key]
        map[Key] = wrapOne(func)
    }
    return map
}

However, I get a following error on wrap(actions):

Argument of type '{ first: (id: number) => void; second: (name: string) => void; }' is not assignable to parameter of type 'Record<"first" | "second", never>'.
  Types of property 'first' are incompatible.
    Type '(id: number) => void' is not assignable to type 'never'.

So, for some reason, it didn't match (id: number) => void with (...args: any) => any, so it infered never.


So I tried a bit different thing:

function wrap2<T, K extends keyof T, U extends VarFn>
(funcList: Record<K, U>) {

    function wrapOne<T extends (...args: any)=>any>(fn: T) {
        return (...args: Parameters<typeof fn>) => {
            someMagicThingyExtra(fn(args))
        }
    }

    type FuncMap = Record<K, (...args: Parameters<U>)=>void>
    let map: FuncMap
    for (var Key in funcList) {
        let func = funcList[Key]
        map[Key] = wrapOne(func)
    }
    return map
}

No errors, but my return type of wrap2(actions) is:

{
    first: (...args: any) => void
    second: (...args: any) => void
}

...and I lost types of parameters, which defeats the whole purpose of trying to wrap the functionality, but preserving signatures (i.e. parameters' types).

Any help or guidance is welcome. Thanks!


EDIT:

Dragomir provided answer that completely preserves signature (both parameters' types and return types). My use case further needed to alter the return type to void and this is how I achieved it:

function wrap<T extends Record<keyof T, (...args: any)=>any>>(funcList: T) {

    // this maps a specific function to a function that does something extra
    function wrapOne<T extends (...args: any) => any>(fn: T) {
        return ((...args: Parameters<typeof fn>): void => {
            someMagicThingyExtra(fn(args))
        })
    }

    // we iterate through the list and map each function to the one that's doing something extra
    type WrapMap = {
        [K in keyof T]: (...args: Parameters<T[K]>)=>void
    }
    let map: WrapMap
    for (var Key in map) {
        map[Key] = wrapOne(funcList[Key])
    }
    return map
}

Solution

  • You generic type T should have a constraint that all it's members are of type VarFn which you can easily so using T extends Record<keyof T, VarFn>. Since the returned type is exactly the same as the input type map can just be of type T.

    type VarFn = (...args: any) => any
    
    function wrap<T extends Record<keyof T, VarFn>>(funcList: T) {
    
      // this maps a specific function to a function that does something extra
      function wrapOne<T extends (...args: any) => any>(fn: T): T {
        return ((...args: Parameters<typeof fn>) => {
          return someMagicThingyExtra(fn(args))
        }) as T
      }
    
      // we iterate through the list and map each function to the one that's doing something extra
      let map = {} as T
      for (var Key in funcList) {
        let func = funcList[Key]
        map[Key] = wrapOne(func)
      }
      return map
    }
    
    const actions = {
      first: (id: number) => {/*...*/ },
      second: (name: string) => {/*...*/ }
    }
    let actionsInterface = wrap(actions)
    
    

    Playground Link