The TypeScript compiler is not able to do much analysis on conditional types that depend on generic type parameters. Since the Parameters<T>
utility type is implemented as a conditional type, and since TFormControl
is an as-yet-unspecified generic type parameter, the compiler really has no idea what Parameters<TFormControl['onChange']>
might be, and whether it might be allowed as the parameter list to a function of type TFormControl['onChange']
. The compiler doesn't give any significance to the identifier Parameters
, and it can't "see into" the definition of Parameters<T>
in an abstract way. If T
is some specific function type, then Parameters<T>
can be evaluated and checked against T
.
So in the compiler's attempt to analyze formControl.onChange(...args)
, it ends up widening formControl
from the generic TFormControl
to the nongeneric FormControl
, which is of the union type ((value: string) => void) | ((value: number) => void)
, and a union of functions is essentially uncallable.
The recommended approach to something like this is to refactor so that your generic operations are represented by indexed accesses into homomorphic mapped types (see What does "homomorphic mapped type" mean? if that term needs explaining) instead of conditional types. This technique is described in detail in microsoft/TypeScript#47109.
First you write a simple mapping object type which connects the literal "TextInput"
and "NumberInput"
types to their corresponding parameter list types:
interface InputArgs {
TextInput: [value: string];
NumberInput: [value: number];
}
Then we can define Input<K>
as a distributive object type over K
(where we index into a mapped type over K
to get a union):
type Input<K extends keyof InputArgs = keyof InputArgs> =
{ [P in K]: { type: P, onChange: (...args: InputArgs[P]) => void } }[K];
The above Input
type subsumes your TextInput
, NumberInput
, and FormControl
types, as shown:
type TextInput = Input<"TextInput">;
type NumberInput = Input<"NumberInput">;
type FormControl = Input;
And now you can write withLogger()
this way:
export const withLogger = <K extends keyof InputArgs>(formControl: Input<K>): Input<K> => ({
...formControl,
onChange: (...args: InputArgs[K]) => {
console.log(formControl.type, 'onChange', args);
return formControl.onChange(...args); // okay
},
});
This works because onChange
for an Input<K>
is explicitly represented as being of type (...args: InputArgs[K]) => void
, and you can therefore pass it an argument list of type InputArgs[K]
. The function and its arguments stay generic and nothing is widened to an unusable union.
Playground link to code