formik icon indicating copy to clipboard operation
formik copied to clipboard

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

Open aliocharyzhkov opened this issue 4 years ago • 20 comments

🐛 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

aliocharyzhkov avatar Mar 06 '20 16:03 aliocharyzhkov

@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

imjakechapman avatar Jul 25 '20 13:07 imjakechapman

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

aliocharyzhkov avatar Jul 27 '20 15:07 aliocharyzhkov

@aliocharyzhkov Can you plz share the workaround?

hassaans avatar Nov 10 '20 05:11 hassaans

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

aliocharyzhkov avatar Nov 10 '20 11:11 aliocharyzhkov

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

philals avatar Nov 25 '20 02:11 philals

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

slutske22 avatar Feb 12 '21 15:02 slutske22

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

devcshort avatar Mar 25 '21 20:03 devcshort

@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 : ''

zapling avatar Apr 20 '21 09:04 zapling

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.

johnrom avatar May 26 '21 22:05 johnrom

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;

MadawaNadun avatar Jun 02 '21 06:06 MadawaNadun

@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 : ''

felix-chin avatar Jul 14 '21 18:07 felix-chin

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.

MadaShindeInai avatar Nov 19 '21 16:11 MadaShindeInai

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.

filip-dahlberg avatar Feb 17 '22 19:02 filip-dahlberg

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

pkasarda avatar Apr 06 '22 09:04 pkasarda

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

alanmatiasdev avatar Apr 06 '22 12:04 alanmatiasdev

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

Mayvis avatar Jun 27 '22 12:06 Mayvis

I am experiencing this error with a Object

Property 'text' does not exist on type 'FormikErrors<{ email: string; password: string; }>

toddhow avatar Sep 13 '22 02:09 toddhow

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

RubyHuntsman avatar May 12 '23 08:05 RubyHuntsman

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')

redaoutamra avatar Nov 01 '23 14:11 redaoutamra

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)}

The-Lone-Druid avatar Nov 18 '23 18:11 The-Lone-Druid