Reputation: 1946
I am trying to make a method that receives an object where the values are methods, and it alters these methods by removing their first parameter. I almost got it working, but for some reason the function parameters and their return values are assigned randomly:
const obj = {
f1: (p11: string, p12: number) => 5,
another: (p21: string, p22: number) => "drt",
aVoid: (p31: string, p32: string) => {}
}
// removes the first argument of a function
type CutFirstArg<F> = F extends (_: any, ...tail: infer TT) => infer R
? (...args: TT) => R
: never
type ValueOf<T> = T[keyof T]
function test<T, K extends CutFirstArg<ValueOf<T>>>(theObject: T){
const result = {}
// some code that automatically alters the first param, this is just an example
for (const [key, value] of Object.entries(theObject)) {
(result as any)[key] = value.bind(null, "firstArmAddedManually", value.arguments.shift())
}
return result as Record<keyof T, K>
}
// this produces an error
const f1 = test(obj).another(21)
How should I type this properly? (also is there a simpler solution? This code looks like spaghetti)
Upvotes: 1
Views: 65
Reputation: 328187
Your version doesn't work because T[keyof T]
produces a union of all the property values in T
, which in your case will be a union of functions. The keys have been forgotten, and any mapping between each key and each function is gone.
What you want instead is a mapped type which keeps track of this, like {[K in keyof T]: CutFirstArg<T[K]>}
, in which each property key K
from theObject
's type is kept, and each property value T[K]
is transformed via CutFirstArg
.
My typing for this would look like:
function test<T extends Record<keyof T, (arg1: string, ...args: any) => any>>(
theObject: T
) {
const result: any = {}
for (const [key, value] of
Object.entries(theObject) as Array<[keyof T, Function]>) {
result[key] = (...args: any) => value("firstArmAddedManually", ...args)
}
return result as { [K in keyof T]: CutFirstArg<T[K]> }
}
Notice that I've constrained the input object to be something whose property values are functions whose first argument is of type string
. This will prevent you from calling something like test({f: (arg: number) => arg.toFixed()})
which would blow up at runtime.
I've also changed some of the type assertions, but most noteworthy here is that I've changed your implementation to something that should actually work. I know you said "this is just an example", but it doesn't work since Function.arguments
only has values inside a call to the function, assuming it works at all which it does not in many environments. Anyway, let's test it:
const testObj = test(obj);
/* const testObj: {
f1: (p12: number) => number;
another: (p22: number) => string;
aVoid: (p32: string) => void;
} */
console.log(test(obj).another(21).toUpperCase()) // DRT
Looks good!
Upvotes: 1
Reputation: 8718
I added a new ObjWithCutFirstArg
type based on your CutFirstArg
type. It's a mapping that basically maps functions to their cut version, and any other value just to themselves, to keep non-function fields of the object intact, in case you care:
const obj = {
f1: (p11: string, p12: number) => 5,
another: (p21: string, p22: number) => "drt",
aVoid: (p31: string, p32: string) => { },
manyArguments: (a: number, b: string, c: boolean) => true,
varargFunc: (a: number, b: string, ...c: boolean[]) => false as const,
noFunction: 123,
}
type ObjWithCutFirstArg<T> = {
[K in keyof T]: T[K] extends (_: any, ...tail: infer TT) => infer R // if a function
? (...args: TT) => R // cut first argument
: T[K]; // or use the original value
};
function test<T>(theObject: T): ObjWithCutFirstArg<T> {
const result = {};
// some code that automatically alters the first param, this is just an example
for (const [key, value] of Object.entries(theObject)) {
(result as any)[key] = value.bind(null, "firstArmAddedManually", value.arguments.shift())
}
return result as any;
}
const mapped = test(obj);
const f1: string = mapped.another(21);
const forIntellisense = { ...mapped };
The intellisense for mapped
displays the following:
Which, when looking at forIntellisense
, actually means the following:
Upvotes: 1