Your OmitFirstArg
seems to be fine, but you need to map it over the properties of your object:
type OmitFirstArgFromMethods<T> = { [K in keyof T]:
T[K] extends (x: any, ...args: infer P) => infer R ? (...args: P) => R : T[K]
}
type ExpectedOutputType<T> = OmitFirstArgFromMethods<InputType<T>>
/* type ExpectedOutputType<T> = {
increment: () => T;
add: (count: number) => T;
} */
By using Record
instead of a mapped type where you act on each property in turn, you're basically throwing away the relationship between the keys and the values.
I'm not sure how to use your test code, though. If get()
is supposed to take an object full of methods-of-at-least-one-argument-of-the-same-type, then you can use a recursive generic constraint to guarantee that input
's properties are all methods of at least one argument, and where the first argument to each is the same type:
function get<T extends { [K in keyof T]:
(state: Parameters<T[keyof T]>[0], ...rest: any) => any
}>(input: T): OmitFirstArgFromMethods<T> {
throw new Error("Not implemented");
}
Then your counter
should have its type inferred, not annotated, since you don't want it to forget that the keys are named increment
and add
(I assume):
const counter = {
increment: (state: CounterType) => {
state.counter++;
return state;
},
add: (state: CounterType, count: number) => {
state.counter += count;
return state;
}
};
And then finally the typings work as I think you want:
const output = get(counter);
output.add(123).counter; // okay
output.increment().counter; // okay
While preventing "wrong" inputs:
get({
foo: "oops" // error, string is not assignable to function
})
get({
bar: (x: string, y: number) => 1, // error, string | number is not string
baz: (x: number) => 2 // error, string | number is not string
})
I'm not 100% sure how you plan to implement get()
, since it needs to pass to the methods of input
a value it doesn't necessarily have. Perhaps get()
should also accept a value of type Parameters<T[keyof T]>[0]
?
Playground link to code