Search code examples
typescripttypescript-typingsredux-form

How to call const function with type arguments?


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;
  1. Can I somehow specify generic arguments to the const function?
  2. Are the typings wrong? How can they be corrected?

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


Solution

  • 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.


    Playground link to code