Definitelytyped: redux-form Field fails compile with type error if custom props on custom component are used

Created on 21 Jul 2017  ·  30Comments  ·  Source: DefinitelyTyped/DefinitelyTyped

  • [x] I tried using the @types/xxxx package and had problems.
  • [x] I tried using the latest stable version of tsc. https://www.npmjs.com/package/typescript
  • [x] I have a question that is inappropriate for StackOverflow. (Please ask any appropriate questions there).
  • [x] [Mention](https://github.com/blog/821-mention-somebody-they-re-notified) the authors (see Definitions by: in index.d.ts) so they can respond.

    • Authors: @CarsonF , @aikoven , @LKay , @bancek

I'm attempting to provide a custom component as the "component" of a redux-form "Field" component.
The custom component accepts a "label" prop which the Field is meant to pass through (similar to the https://codesandbox.io/s/PNQYw1kVy example)

My application is created using create-react-app using react-scripts-ts, using the latest versions of each.

When attempting to use the field with a label prop like so (full code at the end of the issue):
<Field name="name" component={CustomInput} label="Test" />

The following TS error is returned on compile:
(24,50): error TS2339: Property 'label' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<Field> & Readonly<{ children?: ReactNode; }> & Readonly<BaseFieldProps<{}>>'.
I think the problem is that BaseFieldProps is meant to pick up the type of the "component" prop, but instead is using the default {} but I have no idea how to fix this.

Possibly a similar issue to the one found in https://github.com/Microsoft/TypeScript/issues/15463 (which ultimately was the result of the react-redux typings)

import * as React from 'react';
import { Field, reduxForm, InjectedFormProps, WrappedFieldProps } from 'redux-form'

type TextInputProps = {
    label: string,
    className?: string
} & WrappedFieldProps

const TextInput: React.StatelessComponent<TextInputProps> = ({ className, input, label, meta: { touched, error, warning }, ...otherProps }) => {
    return (
        <label className={className} {...otherProps}>
            {label}
            <input {...input} placeholder={label} type="text" />
            {touched && ((error && <div>{error}</div>) || (warning && <div>{warning}</div>))}
        </label>
    )
}

type FormProps = {
    className?: string,
} & InjectedFormProps
const PlainForm: React.StatelessComponent<FormProps> = ({ handleSubmit, className, pristine, submitting, reset }) => (
    <form className={className} onSubmit={handleSubmit}>
        <Field name="name" component={TextInput} label="Test" />
        <button type="submit" disabled={pristine || submitting}>Submit</button>
        <button type="button" disabled={pristine || submitting} onClick={reset}>Reset</button>
    </form>
)

export const ContactForm = reduxForm({
    form: 'itsAForm'
})(PlainForm)

Most helpful comment

@LKay this typecast is almost alien:

const FieldCustom = Field as new () => GenericField<MyFieldCustomProps>;

I doubt anybody installing this typings will be able to use it without having to manually dig the test files. as for a constructive criticism, make a PR with a README.md to explain how to use it after those wild changes. the plus side of using typings is that you can just use them, and learn as you type, not having to rely on typecasting stuff nor reading the typings itself

All 30 comments

@LyraelRayne There is a PR which probably solve this #18276 . Also you can create a custom Field using GenericField for custom component (example for that is in the tests).

@LKay I believe there an issue in WrappedFieldInputProps. onChange method that is supposed to accept a value or an event handler doesn't seem to work as it expected. If you pass a value to onChange, it throws the following exception:

Argument of type 'Date' is not assignable to parameter of type 'ChangeEvent<any>'.
  Property 'target' is missing in type 'Date'

in this example, I tried to pass a value:Date to onChange method to change the value of the field.
here is the EventOrValueHandler interface:

interface EventOrValueHandler<Event> {
    (event: Event): void;
    (value: FieldValue, newValue: FieldValue, previousValue: FieldValue): void;
}

I believe (value: FieldValue, newValue: FieldValue, previousValue: FieldValue): void; should be something like:
(value:FieldValue)

You might be partially rigth with that. It sems this was changed and the first parameter passed to the handler is always an event. http://redux-form.com/7.0.1/docs/api/Field.md/#-code-onchange-event-newvalue-previousvalue-gt-void-code-optional- and https://github.com/erikras/redux-form/blob/066e75356cc1e43ce22755337c48d80bc16a49af/src/ConnectedField.js#L103-L115

I will change it asap.

@LKay, The first parameter is not always an event. onChange can be used to update the value of a field as well. So, it could be either an event or just a new value for the field : http://redux-form.com/7.0.1/docs/api/Field.md/#-code-input-onchange-eventorvalue-function-code-

These are examples from their repository where onChange method is used to update the value of a field:
https://github.com/erikras/redux-form/blob/82259fbac0a53b583c2b52738522fedbd35f8c40/examples/material-ui/src/MaterialUiForm.js#L54-L76
and also, https://github.com/erikras/redux-form/blob/e9bcc1b54e211071f0cefc644c1ab7b2e93b9d78/docs/faq/CustomComponent.md

So, basically both onChange and onBlur need to be changed to:
onBlur(eventOrValue: SyntheticEvent | FieldValue): void;
onChange(eventOrValue: SyntheticEvent | FieldValue): void;

@rezanouri87 I played with the code and basically the only thing that needed to be changed is to replace onFocus and onDragStart with regular event handlers as they don't take value as parameter but only an event. For other handlers the signature is correct. Handler for onChange can take both event or field value but signature also accepts next and previous value as 2nd and 3rd parameters, so it can't be changed to onChange(eventOrValue: SyntheticEvent | FieldValue): void;.

I tried to pass values to handlers and seems to be fine in my code. Can you isolate your problem and share it on gits or fiddle?

the provided tests have no mention of "label" props, and this is clearly a regression on the typings (and broke my entire project). it should at least have tests that mirror the official redux-form docs from http://redux-form.com/7.0.1/examples/syncValidation/, like so:

const SyncValidationForm = props => {
  const { handleSubmit, pristine, reset, submitting } = props
  return (
    <form onSubmit={handleSubmit}>
      <Field
        name="username"
        type="text"
        component={renderField}
        label="Username"
      />
      <Field name="email" type="email" component={renderField} label="Email" />
      <Field name="age" type="number" component={renderField} label="Age" />
      <div>
        <button type="submit" disabled={submitting}>
          Submit
        </button>
        <button type="button" disabled={pristine || submitting} onClick={reset}>
          Clear Values
        </button>
      </div>
    </form>
  )
}

the current tests cover almost no code from the official documentation, and will make it hard for newcomers to use it. there are an unnecessary amount of typecasting and manual setting of props that needs to be done when using this types, when some common documented props should come by default (just like label prop)

@pocesar, I believe here is the piece of code that you are looking for:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/redux-form/redux-form-tests.tsx#L82-L93
As you may notice, property "foo" was added as a custom field, which is label in your case, to the MyFieldCustomProps type.
I am using the same solution and it works fine.

@rezanouri87 I know you can do that, but shouldn't be necessary. the types should have the common props (in BaseFieldProps or CommonFieldProps) that are already documented in the official docs, that's my point

@pocesar This actually should be necessary if you want to benefit from using use Typescript and have your code type safe. The property label is not a property available on original Field component so you should either provide a custom component as component property to let compiler resolve the type which contains label or define custom field the way @rezanouri87 mentioned using GenericField interface.

@LKay this typecast is almost alien:

const FieldCustom = Field as new () => GenericField<MyFieldCustomProps>;

I doubt anybody installing this typings will be able to use it without having to manually dig the test files. as for a constructive criticism, make a PR with a README.md to explain how to use it after those wild changes. the plus side of using typings is that you can just use them, and learn as you type, not having to rely on typecasting stuff nor reading the typings itself

@LKay I believe that I have the latest typings which I think might include that PR. I still get a (different) error though (see below). I figured out how to use GenericField and that seems to work, but I certainly would rather not have to do that if possible.

I'm with pocesar on const FieldCustom = Field as new () => GenericField<MyFieldCustomProps>; being confusing. I really don't understand what as new() is doing and would certainly not have thought to do so on my own.

The error I get with the new types file is

severity: 'Error'
message: 'Property 'label' does not exist on type '(IntrinsicAttributes & IntrinsicClassAttributes<Field<GenericFieldHTMLAttributes>> & Readonly<{ children?: ReactNode; }> & Readonly<BaseFieldProps<GenericFieldHTMLAttributes> & InputHTMLAttributes<HTMLInputElement>>) | (IntrinsicAttributes & IntrinsicClassAttributes<Field<GenericFieldHTMLAttributes>> & Readonly<{ children?: ReactNode; }> & Readonly<BaseFieldProps<GenericFieldHTMLAttributes> & SelectHTMLAttributes<HTMLSelectElement>>) | (IntrinsicAttributes & IntrinsicClassAttributes<Field<GenericFieldHTMLAttributes>> & Readonly<{ children?: ReactNode; }> & Readonly<BaseFieldProps<GenericFieldHTMLAttributes> & TextareaHTMLAttributes<HTMLTextAreaElement>>)'.'
at: '24,50'
source: 'ts'

I'm certainly glad I don't have to write these things myself (thanks guys!), but I would definitely appreciate anything that can make it a little easier to use.

@LyraelRayne Your type that you're passing to GenericField most likely doesn't contain definition for label, can't tell more without seeing the actual code.

@rezanouri87 Well, that hasn't changed from what it was in typings for v6, definition for custom fields was almost exactly the same. Type Field takes type for component properties, but ideally this _should_ add this type on the fly to components props. Unfortunately this is not possible as for current version of Typesctipt, so you have to define it yourself. In the previous version everything was passing any everywhere resulting in no type checking at all. This is definitely not coming back.

@LKay I am referring to what happens when using the original code (at the top of this issue), not using GenericField. GenericField is working.

Why would we want to make a generic for using the normal Field. Material-UI makes use of the props.label for creating a functional TextField. When the label is removed you break material-ui's use. @LKay @rezanouri87 @pocesar http://redux-form.com/6.0.0-rc.1/examples/material-ui/

I switched to use props.title instead since that is available. But like @pocesar said this will break peoples code.

You're not making a generic, you're casting the existing Field to a more specific type so that Typescript understands what the props are (because type inference fails). It's annoying and not a pleasant way to work with the redux-form framework, but it's functional. I'm hoping that at some stage there will be a solution so we don't need to deal with that, but as I am not in a position to propose said solution I've accepted it for now.

One thing I will mention is that if you're willing to give up type safety, you can probably do the following in a module.

// untyped-field.ts
import {Field, GenericField} from "redux-form"
const UntypedField = Field as new () => GenericField<any>;
export {UntypedField as Field}

now if you import Field from untyped-field.ts instead of redux-form you'll be able to use whatever props you like, but your compiler/tooling won't tell you if you pass garbage in.

This isn't a proper solution, but it does allow you to use Typescript in the rest of your application without having to fight with/work around type inference with redux-form.

It would be very helpful if a set of good examples would be provided!

Since it both is an important deviation from official documentation, and it includes some pretty confusing syntax hidden in the test files.

I still haven't figured out how to this :-)

Shouldn't it be possible to pass in a custom label prop to the standard Field component using the props prop in version 7?

"props : object [optional]
Object with custom props to pass through the Field component into a component provided to component prop. This props will be merged to props provided by Field itself. This may be useful if you are using TypeScript."
http://redux-form.com/7.0.4/docs/api/Field.md/

I get the following error trying this:

`

error TS2322: Type '{ type: "text"; name: "fullName"; component: typeof Input; props: { label: string; }; }' is not assignable to type '(IntrinsicAttributes & IntrinsicClassAttributes> & Readonly<{ c...'.
Type '{ type: "text"; name: "fullName"; component: typeof Input; props: { label: string; }; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes> & Readonly<{ ch...'.
Type '{ type: "text"; name: "fullName"; component: typeof Input; props: { label: string; }; }' is not assignable to type 'Readonly & TextareaHTMLAttributes>'.
Types of property 'props' are incompatible.
Type '{ label: string; }' is not assignable to type 'InputHTMLAttributes | SelectHTMLAttributes | TextareaHTMLAtt...'.
Property 'label' does not exist on type 'InputHTMLAttributes | SelectHTMLAttributes | TextareaHTMLAtt...'.`

yeah, this was by far the worst PR made to this typings so far, broke huge forms without the need for it

any newd about it
I have the same issue when uisng typescript

You could do something like this:

const LabelComponent = (label: string) => { return ({input, type, children, meta: {touched, error}}) => ( <div> <label>{label}</label> <div> <input {...input} type={type} placeholder={label}/> {touched && error && <span>{error}</span>} </div> </div> ); };

and then inside your form
<Field name="quantity" type="number" component={LabelComponent("Quantity")} /> <Field name="price" type="number" component={LabelComponent("Price")} />

To bridge the gap while we wait for typings that aren't broken, you can use type augmentation:

@types/recompose.d.ts

import * as React from "react"
import { BaseFieldProps, GenericField, WrappedFieldProps } from "redux-form"

declare module "redux-form" {
  export class Field<P = any> extends React.Component<BaseFieldProps<P> & P> implements GenericField<P> {
    dirty: boolean;
    name: string;
    pristine: boolean;
    value: any;
    getRenderedComponent(): React.Component<WrappedFieldProps & P>;
  }
}

tsconfig.json

{
  ...
  "include": [
      "./@types/**/*.d.ts",
      "./src/**/*"
    ]
   ...
}

edit: looks like you'll need to do the same thing for Fields and FieldArray

Another example of usage for v7.0.4 - a wrapper for the Field component with more or less correct prop types for it. Here I'm using an intersection (&) of html attr types instead of a union (|).

I'd say there should be an easier way, perhaps a separate type for Field's props? Or maybe I'm missing something

import React, { 
  InputHTMLAttributes, 
  SelectHTMLAttributes, 
  TextareaHTMLAttributes 
} from 'react';
import { Box, Label } from 'rebass';
import { Field, BaseFieldProps } from 'redux-form';

declare type Props = {
  label: string;
};

declare type GenericFieldHTMLAttributes = InputHTMLAttributes<HTMLInputElement> &
  SelectHTMLAttributes<HTMLSelectElement> &
  TextareaHTMLAttributes<HTMLTextAreaElement>;

declare type AllProps = Props & GenericFieldHTMLAttributes & BaseFieldProps;

export default ({ label, children, ...rest }: AllProps) => (
  <Label wrap="wrap" mb={2}>
    <Box w={[1, 1 / 3]}>{label}</Box>
    <Box w={[1, 2 / 3]}>
      <Field {...rest}>{children}</Field>
    </Box>
  </Label>
);

@airato Thanks a lot for this solution, it helps

However typings still break if using Field props prop, any to rescue..

          <Field
            name="diplayUnit"
            component={Fields.DisplayUnit}
            props={{
              options: dropdownOptions
            } as any}
          />

Having the same issue, I have to do:
<Field name="diplayUnit" component={Fields.DisplayUnit} {{ options: dropdownOptions} as any} />
This is annoying

My version and seems it works:

import * as React from 'react';
import { Field as BaseField, reduxForm, InjectedFormProps, WrappedFieldProps } from 'redux-form';

interface FieldProps {
    label: string;
    yourProperty?: string;
}

class Field extends BaseField<FieldProps & React.InputHTMLAttributes<HTMLInputElement>> {}

const TextBox = (props: FieldProps & React.InputHTMLAttributes<HTMLInputElement> & WrappedFieldProps) => {
    const { input, type, label, yourProperty, meta } = props;
    const { touched, error } = meta;

    return <div>
        <label>{label}{yourProperty}:</label>
        <div>
        <input {...input} placeholder={label} type={type}/>
        {touched && (error && <span>{error}</span>)}
        </div>
    </div>
}

export class MyProfile extends React.Component<InjectedFormProps> {
    public render() {
        const { handleSubmit } = this.props;

        return <form onSubmit={handleSubmit}>
            ...
            <Field name="username" type="text" component={TextBox} label="Username" yourProperty="*" />
            ...
        </form>;
    }
}

export default reduxForm({
    form: 'myProfileForm'
})(MyProfile);

I've checked more carefully messages in the beginning of this thread and it looks like the proper way to use custom fields is like the following. What's good you don't have to uses 'props' prop anymore as well as typing hacks:

    <MeasureField
          allMeasures={field.measures} // custom field !
          name={`${member}.measureId`}
          component={Measure}
    />
type MeasureOwnProps = { allMeasures: Measure[] };
type MeasureProps = WrappedFieldProps & MeasureOwnProps;

const MeasureField = Field as new() => GenericField<MeasureOwnProps>;

const Measure: React.SFC<MeasureProps> = props => {

  const { allMeasures, input } = props;
  const { onChange, ...rest } = input;

  return (
    <div className={style.measure}>
      <Dropdown
        inline
        pointing='top right'
        options={measureOptions(allMeasures)}
        onChange={(e, { value }) => onChange(value)}
        {...rest}
      />
    </div>
  );
};

Thanks @rasdaniil ..
There is example for someone, who want to use redux-form Field with react-native TextInput.

Found something,

You also have to declare your now Field, ie:

import { Field } from 'redux-form';

interface MyOwnProps {
options :any
}

class MyField extends Field<MyOwnProps> {}

export default MyField;

and use it as

<MyField
   name="diplayUnit"
    component={Fields.DisplayUnit}
   options ={whatev}
/>
Was this page helpful?
0 / 5 - 0 ratings