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
}
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)