2

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
}

3 Answers 3

3

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

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

2 Comments

I am impressed that if manages to infer the correct type of functions instead of leaving them as VarFn. I guess extending entire Record instead of only its value part does the trick. This does manage to solve my problem. Once more thing, my returned functions should return void once wrapped. How that can be done? Thanks.
I've managed to succeed with changing the return type to void. Edited question to include it as well, and it was based on your answer. Thanks!
1

This is the first thing I landed on. It keeps even the generics of the wrapped function:

const newFunction = ((one, two) => {
    const result = oldFunction(one, two)
    // ...
    return result
}) as typeof oldFunction

Comments

0

If you want to wrap a function preserving the individual param types, you can like so:

const wrap = <Args extends any[], ReturnType>(fun: (...args: Args) => ReturnType) =>
    (...args: Args) =>
        fun(...args)

// Example:
const expectsCallback = (cb: (arg1: string, arg2: number) => boolean) => 0
expectsCallback(
    wrap(
        // Correctly typed here
        (a1, a2) => true
    )
)

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.