Given this definition from the redux-form
typings:
export type DataSelector<FormData = {}, State = {}> = (formName: string, getFormState?: GetFormState) => (state: State) => FormData;
export const getFormValues: DataSelector;
Note that DataSelector
has two type arguments. If I am calling getFormValues
, which is a const
function, how do I supply the type arguments?
type FormFields = { name: string };
// No type argument specified so: Property 'name' does not exist on type '{}'.
const name = getFormValues('my-form')(state)?.name;
// I could cast it, but no thank you ...
const name = (getFormValues('my-form')(state) as any as FormFields)?.name;
// Need to supply `FormFields` as the generic argument for `FormData`, but how? This doesn't work ...
const name = getFormValues<FormFields>('my-form')(state)?.name;
const
function?Update: The typings in this case are incorrect. Instead of...
export type DataSelector<FormData = {}, State = {}> = (formName: string, getFormState?: GetFormState) => (state: State) => FormData;
...it should be...
export type DataSelector = <FormData = {}, State = {}>(formName: string, getFormState?: GetFormState) => (state: State) => FormData;
...(generic arguments on the right side of the =
) with this change, this is now valid:
const name = getFormValues<FormFields>('my-form')(state)?.name;
PR opened: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/49896
The problem here is that getFormValues
is not a generic function, so there's no direct way to use it for your purposes. The type DataSelector
is the same as DataSelector<{}, {}>
because the definition of DataSelector
uses type parameter defaults. So getFormValues
essentially has the following typing:
declare const getFormValues: (
formName: string, getFormState?: GetFormState
) => (state: {}) => {};
There's nothing generic in there anymore. If you call getFormValues()
it returns a function of type (state: {}) => {}
which accepts (just about) anything and returns {}
, an object type with no known properties.
I'm not really sure what the intended use of DataSelector
is, nor how getFormValues()
is implemented, so the following is just the typings I might use to get something that works for your particular example code.
If you want getFormValues
itself to be a generic function, you'd need to move the generic type parameters off of the DataSelector
type declaration and onto the call signature, like this:
type GenericDataSelector = <FormData = {}, State = {}>(
formName: string, getFormState?: GetFormState
) => (state: State) => FormData;
and then have getFormValues
be a value of that type:
const getFormValuesGeneric = getFormValues as GenericDataSelector;
This will give you the behavior you want, I think:
const nameOkay = getFormValuesGeneric<FormFields>('my-form')(state).name;
That works, although I'm a bit skeptical of generic functions without inference sites for the type parameters. The GenericDataSelector
type's call signature is generic in two parameters, neither of which appear in the parameters of the call signature... they only appear in the return type.
That means if I call getFormValuesGeneric("something")
, the compiler has no idea how to infer what State
and FormData
should be, and it ends up defaulting to {}
and {}
. If you want something different you need to manually specify it, like getFormValuesGeneric<MyFormData, MyState>("something")
.
That works, but you end up in the weird position of saying that getFormValuesGeneric<FormData1, State1>("something")
returns a function of type (state: State1) => FormData1
while getFormValuesGeneric<FormData2, State2>("something")
returns a function of type (state: State2) => FormData2
. But of course at runtime the type system has been erased, so both of those are just getFormValuesGeneric("something")
. How can two identical calls return two different function types that depend on stuff that's been erased? They can't, really.
Meaning that at most one of <FormData1, State1>
or <FormData2, State2>
is the right choice for getFormValuesGeneric("something")
... and choosing the correct one is, I guess, the responsibility of the person writing the TS code that calls the function. But in that case there's not much of a difference between that and using a type assertion (what you called a "cast"); neither of them guarantee any kind of type safety.
Presumably there's some relationship between the value of the formName
parameter passed in and the correct specifications for FormData
and State
, but this is not known to the compiler. One could imagine rewriting the typings so that the compiler is able to keep track of this mapping, but I consider that well outside the scope of this question.
The takeaway from all of this is just BE CAREFUL when using generic functions whose parameters can't reasonably be inferred from anything.