jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
33.98k stars 2.79k forks source link

Incorrect type inference for members of FormikErros which are elements of an array of objects #2347

Open aliocharyzhkov opened 4 years ago

aliocharyzhkov commented 4 years ago

🐛 Bug report

Current Behavior

Suppose we have the following types:

interface IMember {
  name: string;
}

interface IGroup {
  name: string;
  members: IMember[];
}

Then the type of errors.members[i] is inferred as 'string | FormikErrors<IMember>' which produces this error Property 'name' does not exist on type 'string | FormikErrors<IMember>'. when one tries to access errors.members[i].name.

Expected behavior

The type of errors.members[i] should be 'FormikErrors<IMember>'.

Reproducible example

Notice the error on line 57: https://codesandbox.io/s/react-typescript-uucgn

Suggested solution(s)

The solution is to delete | string | string[] in this line.

Additional context

Your environment

Software Version(s)
Formik 2.1.3
React 16.12.0
TypeScript 3.8.3
Browser
npm/Yarn
Operating System
imjakechapman commented 4 years ago

@aliocharyzhkov Did you resolve this? Looks like it's become stale.

If not, I'd love to have someone either take a deeper look for a fix or allow a PR to get merged for this.

I have a nested array using Yup validation and this breaks typescripts ability to compile.

Compile error

const hasErrors = errors?.permutations && errors.permutations[groupIdx].sets[setIdx].length // Property 'sets' does not exist on type 'string | FormikErrors<PermutationGroup>'.

Render error

<ErrorMessage name={`permutations[${groupIdx}].sets[${setIdx}]`} /> // Objects are not a valid react child
aliocharyzhkov commented 4 years ago

@imjakechapman I've used a temporary workaround. The issue hasn't been resolved in the library yet, so I went ahead and created a PR. I hope it will facilitate fixing it.

hassaans commented 4 years ago

@aliocharyzhkov Can you plz share the workaround?

aliocharyzhkov commented 4 years ago

@hassaans I use a ternary operator to check if there's an error, and if there is I cast the object to the correct type. For example, errors.memebers[i] ? (errors.members[i] as IMember).name : ''. Fortunately, there are just a few places where I have to use this hack. Moreover, I added a comment to remind me to refactor those line when the issue is fixed in the library.

philals commented 3 years ago

Related: https://github.com/formium/formik/issues/2396

slutske22 commented 3 years ago

I just ran into this problem yesterday and it was a huge pain. Why is this labeled stale?

devcshort commented 3 years ago

@hassaans I use a ternary operator to check if there's an error, and if there is I cast the object to the correct type. For example, errors.memebers[i] ? (errors.members[i] as IMember).name : ''. Fortunately, there are just a few places where I have to use this hack. Moreover, I added a comment to remind me to refactor those line when the issue is fixed in the library.

I appreciate you providing the workaround. really hoping this gets fixed in the near future.

zapling commented 3 years ago

@hassaans I use a ternary operator to check if there's an error, and if there is I cast the object to the correct type. For example, errors.memebers[i] ? (errors.members[i] as IMember).name : ''. Fortunately, there are just a few places where I have to use this hack. Moreover, I added a comment to remind me to refactor those line when the issue is fixed in the library.

Thank you. I needed to modify it a bit to get rid of possible undefined.

errors.members && errors.memebers[i] ? (errors.members[i] as IMember).name : ''
johnrom commented 3 years ago

You can track errors anywhere inside of an object or array tree. For example, if I had a field called name in the shape of an object with name.first and name.last, I can

setFieldError("name", "please enter a first or last name.")

You can determine let typescript determine whether your error is a string or not with:

const error = typeof errors.yourObject === 'string'
  ? errors.yourObject
  : errors.yourObject.yourSubValue

Or various other types of type guarding. (isObject, Array.isArray), Unless I'm mistaken, this is working as intended.

MadawaNadun commented 3 years ago

this works for me i combided @aliocharyzhkov work around

i am using formik yup and material ui

{(formik.errors.contact && formik.touched.contact) && (formik.touched.contact[i])?.number && (formik.errors.contact[i])?.number}

import React, { useState } from "react";
import { useFormik } from 'formik';
import * as yup from 'yup';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';

// reactstrap components
import {
  Card,
  CardHeader,
  CardBody,
  CardFooter,
  Container,
  FormGroup,
  Form,
  Button,
  Row,
} from "reactstrap";

import { TextField, Grid } from '@material-ui/core';

import { createContact, updateContact } from '../../actions/contact';

const phoneRegExp = /^((\\+[1-9]{1,4}[ \\-]*)|(\\([0-9]{2,3}\\)[ \\-]*)|([0-9]{2,4})[ \\-]*)*?[0-9]{3,4}?[ \\-]*[0-9]{3,4}?$/;

const validationSchemaContact = yup.object({
    title: yup
        .string('Enter contact title')
        .max(255, 'Contact title should be of maximum 255 characters length')
        .required('Contact title is required'),
    contact: yup
        .array()
        .of(yup
          .object({
            person: yup
              .string('Enter contact person name')
              .min(3, 'Address should be of minimum 3 characters length')
              .max(255, 'Contact person name should be of maximum 255 characters length'),
            number: yup
              .string('Enter contact person phone number')
              .matches(phoneRegExp, 'Phone number is not valid')
              .required('Phone number is required'),
          })
          .required('Required'),
        )
  });

const ContactForm = () => {
  // dispatch and history
  const dispatch = useDispatch();
  const history = useHistory();
  // set state contact number list
  const [contactList, setContactList] = useState([{ person: "", number:"" }]);

  // handle change event of the contact number
  const handleContactChange = (e, index) => {
    const { name, value } = e.target;
    const list = [...contactList];
    list[index][name] = value;
    setContactList(list);
    formik.values.contact =  contactList;
  };

  // handle click event of the Remove button of contact number
  const handleRemoveClick = index => {
    const list = [...contactList];
    list.splice(index, 1);
    setContactList(list);
  };

  // handle click event of the Add button of contact number
  const handleAddClick = () => {
    setContactList([...contactList, { person: "", number:"" }]);
  };

  const formik = useFormik({
    initialValues: {
      title: '',
      contact: [{
        person:'',
        number:''
      }]
    },
    validationSchema: validationSchemaContact,
    onSubmit: (values, { resetForm }) => {
      dispatch(createContact(values, history));
      resetForm();
    },
    onReset: (values, { resetForm }) => resetForm(),
  });

  return (
    <>
      <Header />
      {/* Page content */}
      <Container className="mt--7" fluid>
        {/* Table School */}
        <Row>
          <div className="col">
            <Card className="shadow">
              <CardHeader className="border-0">
                <h3 className="mb-0">Contact Details</h3>
              </CardHeader>
              <Form role="form" onSubmit={formik.handleSubmit}>
                <CardBody>
                  <FormGroup className="mb-3">
                    <Grid container spacing={1} alignItems="center">
                      <Grid item  xs={1} sm={1}>
                        <i className="fas fa-heading" />
                      </Grid>
                      <Grid item xs={11} sm={11}>
                        <TextField 
                          fullWidth
                          id="title" 
                          name="title" 
                          label="Title" 
                          variant="outlined"
                          value={formik.values.title}
                          onChange={formik.handleChange}
                          error={formik.touched.title && Boolean(formik.errors.title)}
                          helperText={formik.touched.title && formik.errors.title}
                        />
                      </Grid>
                    </Grid>
                      {contactList.map((x, i) => {
                        return (
                          <Grid key={i} container spacing={1} alignItems="center">
                            <Grid item  xs={1} sm={1}>
                              <i className="ni ni-mobile-button">{(1 + i)}</i>
                            </Grid>
                            <Grid item xs={10} sm={10}>
                              <TextField style={{width:'50%'}}
                                id="person" 
                                name="person" 
                                label={'Contact Person'}
                                variant="outlined"
                                value={x.person}
                                onChange={e => handleContactChange(e, i)} 
                                error={(formik.errors.contact && formik.touched.contact) && (formik.touched.contact[i])?.person && Boolean((formik.errors.contact[i])?.person)}
                                helperText={(formik.errors.contact && formik.touched.contact) && (formik.touched.contact[i])?.person && (formik.errors.contact[i])?.person}
                              />
                              <TextField style={{width:'50%'}}
                                id="number" 
                                name="number" 
                                label={'Contact Number'}
                                variant="outlined"
                                value={x.number}
                                onChange={e => handleContactChange(e, i)} 
                                error={(formik.errors.contact && formik.touched.contact) && (formik.touched.contact[i])?.number && Boolean((formik.errors.contact[i])?.number)}
                                helperText={(formik.errors.contact && formik.touched.contact) && (formik.touched.contact[i])?.number && (formik.errors.contact[i])?.number}
                              />
                            </Grid>
                            <Grid item  xs={1} sm={1} >
                              {contactList.length !== 1 && <RemoveCircleOutlineIcon className="mr10" onClick={() => handleRemoveClick(i)}/>}
                              {(contactList.length - 1 === i && !(contactList.length > 4)) && <AddCircleOutlineIcon className="ma10" onClick={handleAddClick}/>}
                            </Grid>
                          </Grid>
                        );
                      })}
                  </FormGroup>
                </CardBody>
                <CardFooter className="py-2 text-right">
                  <Button className="my-2" variant="contained" color="primary" type="submit">
                    Submit
                  </Button>
                </CardFooter>
              </Form>
            </Card>
          </div>
        </Row>
      </Container>
    </>
  );
};

export default ContactForm;
felix-chin commented 3 years ago

@hassaans I use a ternary operator to check if there's an error, and if there is I cast the object to the correct type. For example, errors.memebers[i] ? (errors.members[i] as IMember).name : ''. Fortunately, there are just a few places where I have to use this hack. Moreover, I added a comment to remind me to refactor those line when the issue is fixed in the library.

So glad I found this issue, I thought I was going crazy with this error. Thank you for reporting this and posting the workaround.

For possible undefined, optional chaining will work: errors.members?.[i] ? (errors.members[i] as IMember).name : ''

MadaShindeInai commented 2 years ago

Faced it today. And yes, I thought I was going crazy with this error too. Maybe there should be added a warning to docs or something?

error={touched.test && Boolean((errors.test?.[0] as ErrorsAmount)?.amount)}
helperText={(errors.test?.[0] as ErrorsAmount)?.amount}

That worked for me.

filip-dahlberg commented 2 years ago

There exists an undocumented function GetIn in fomik that one can use to retrieve the form error in case it's located in a nested object. This is how I solved it.

pkasarda commented 2 years ago

@filip-dahlberg it's documented https://formik.org/docs/api/fieldarray#fieldarray-validation-gotchas

alanmatiasdev commented 2 years ago

@filip-dahlberg you use the getIn function as indicated in the documentation by @peter-visma ?

update: I tested it in a project using getIn and it solved my problem.

Mayvis commented 2 years ago

Both getIn(errors.col?.[index], 'key') and (errors.test?.[index]) as SomeType)?.[key] are work very well. ;)

toddhow commented 2 years ago

I am experiencing this error with a Object

Property 'text' does not exist on type 'FormikErrors<{ email: string; password: string; }>
RubyHuntsman commented 1 year ago

Are there any chances for a better solution to this problem?

redaoutamra commented 1 year ago

Are there any chances for a better solution to this problem?

There is a better solution using 'getIn,' which is not explicitly mentioned in the documentation, except when used in conjunction with something else, as shown here: https://formik.org/docs/api/connect. An example usage is as follows: getIn(errors, 'user.[${index}].email')

The-Lone-Druid commented 12 months ago

error={(formik.errors.contact && formik.touched.contact) && (formik.touched.contact[i])?.number && Boolean((formik.errors.contact[i])?.number)}

This one worked well for me

error={(formik.errors.contact && formik.touched.contact) && (formik.touched.contact[i])?.number && Boolean((formik.errors.contact[i])?.number)}