Typescript — Введите функцию, которая принимает массив, изменяет одно из его свойств, но все равно возвращает тот же типизированный массив

#javascript #typescript

#javascript #typescript

Вопрос:

У меня есть поля формы, представленные в виде объектов, которые отображаются поверх и на основе type возвращаемых элементов React:

 import { FieldProps } from "~types";

const fields: FieldProps[] = [
  { type: "text", name: "username", value: "", required: true, ...other props },
  { type: "password", name: "password", value: "", required: true, ...other props },
  ...etc
]

export default fields;
  

Проблема, с которой я сталкиваюсь, заключается в том, что я пытаюсь проверить поля после отправки формы и проверить наличие ошибок в value :

  handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    const { validatedFields, errors } = fieldValidator(this.state.fields);

    if(errors) {
      this.setState({ errors });
      return;
    }

    ...etc
 }
  

но эта повторно используемая функция проверки имеет ошибку ts:

 import isEmpty from "lodash.isempty";

/**
 * Helper function to validate form fields.
 *
 * @function
 * @param {array} fields - an array containing form fields.
 * @returns {object} validated/updated fields and number of errors.
 * @throws {error}
 */

const fieldValidator = <
 T extends Array<{ type: string; value: string; required?: boolean }>
>(
  fields: T,
): { validatedFields: T; errors: number } => {
  try {
    if (isEmpty(fields)) throw new Error("You must supply an array of form fields to validate!");
    let errorCount: number = 0;

    // this turns the "validatedFields" array into an { errors: string; type: string; name: 
    // string; value: string; required?: boolean | undefined;}[] type, when it needs to be "T", 
    // but "T" won't be inferred as an Array with Object props: type, value, required, value defined within it
    const validatedFields = fields.map(field => {
      let errors = "";
      const { type, value, required } = field;
      if ((!value amp;amp; required) || (isEmpty(value) amp;amp; required)) {
        errors = "Required.";
      } else if (
        type === "email" amp;amp;
        !/^[A-Z0-9._% -] @[A-Z0-9.-] .[A-Z]{2,4}$/i.test(field.value)
      ) {
        errors = "Invalid email.";
      }

      if (errors) errorCount  = 1;

      return { ...field, errors };
    });

    return { validatedFields, errors: errorCount }; // TS error here
  } catch (err) {
    throw String(err);
  }
};

export default fieldValidator;
  

Поскольку validatedFields превращается в:

 {
    errors: string;
    name: string;
    value: string;
    type: string;
    required: boolean;
}[]
  

и он возвращается { validatedFields, errors } , он выдает эту ошибку TS:

 Type '{ errors: string; type: string; value: string; required?: boolean | undefined; }[]' is not assignable to type 'T'.
  '{ errors: string; type: string; value: string; required?: boolean | undefined; }[]' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ type: string; value: string; required?: boolean | undefined; }[]'.ts(2322)
index.ts(17, 6): The expected type comes from property 'validatedFields' which is declared here on type '{ validatedFields: T; errors: number; }'
  

Есть ли способ вывести T как массив объектов, который ожидает по крайней мере 4 (или более) свойства, но возвращает тот же типизированный массив (только с обновленным errors свойством)?

Typescript Playground

Ответ №1:

Это то, что вы ищете?

 type EssentialField = { type: string; value: string; required?: boolean }[];
  

В Typescript используется утиный ввод.

 type FieldProps = {
  className?: string,
  disabled?: boolean,
  errors?: string,
  placeholder?: string,
  label: string,
  // onChange?: (e: React.ChangeEvent<any>) => void;
  name: string,
  type: string,
  readOnly?: boolean,
  required: boolean,
  value: string
  styles?: object
};


const fields: FieldProps[] = [
  { 
    type: "text", 
    label: "Username", 
    name: "username", 
    errors: "",
    value: "", 
    required: true,
  },
  { 
    className: "example",
    type: "password", 
    label:"Passowrd", 
    name: "password", 
    errors: "", 
    value: "", 
    required: true, 
    styles: { width: "150px" },
  }
];

type EssentialField = { type: string; value: string; required?: boolean }[];

const fieldValidator = (
  fields: EssentialField,
): { validatedFields: EssentialField; errors: number } => {
  try {
    if (!fields || fields.length < 0) throw new Error("You must supply an array of fields!");
    let errorCount: number = 0;

    const validatedFields = fields.map(field => {
      let errors = "";
      const { type, value, required } = field;
      if ((!value amp;amp; required) || ((value amp;amp; value.length < 0) amp;amp; required)) {
        errors = "Required.";
      } else if (
        type === "email" amp;amp;
        !/^[A-Z0-9._% -] @[A-Z0-9.-] .[A-Z]{2,4}$/i.test(field.value)
      ) {
        errors = "Invalid email.";
      }

      if (errors) errorCount  = 1;

      return { ...field, errors };
    });

    return { validatedFields, errors: errorCount };
  } catch (err) {
    throw String(err);
  }
};

const { validatedFields, errors } = fieldValidator(fields)
console.log(validatedFields)
console.log(errors);
  

Это устраняет ошибку без использования as any или других трюков.

РЕДАКТИРОВАТЬ: Может быть, вы хотите что-то подобное:

 type EssentialField = ({ type: string; value: string; required?: boolean }amp;{[key: string]: any})[];

// this allow you to do:
validatedFields[0].placeholder; // ...
// or
validatedFields[0].someUnknowProperty; // ...
  

Это пересечение типа с ассоциативным массивом ( {[key: string]: any}) ). Обычно к нему обращаются подобным образом associativeArray['someKey'] , но его также можно использовать подобным образом associativeArray.someKey .

Преимущество заключается в том, что ваша среда разработки должна предлагать вам поле type и required в автозаполнении.

Комментарии:

1. Это близко, но ни одно из приведенных выше решений не сохраняет другие типы, расположенные внутри FieldProps[] . Например, validatedFields[0].label (который требуется, см. Typescript playground) не вводится и не предоставляется в автозаполнении после проверки.

Ответ №2:

Мне удалось выяснить это, расширив T до any[] , а затем определив результат validatedFields as T , чтобы сохранить переданные типы:

 const fieldValidator = <
  T extends any[]
>(
  fields: T,
): { validatedFields: T; errors: number } => {
  try {
    if (!fields || fields.length < 0) throw new Error("You must supply an array of fields!");
    let errorCount: number = 0;

    const validatedFields = fields.map(field => {
      let errors = "";
      const { type, value, required }: Pick<BaseFieldProps, "type" | "value" | "required"> = field;
      if ((!value amp;amp; required) || ((value amp;amp; value.length < 0) amp;amp; required)) {
        errors = "Required.";
      } else if (
        type === "email" amp;amp;
        !/^[A-Z0-9._% -] @[A-Z0-9.-] .[A-Z]{2,4}$/i.test(value)
      ) {
        errors = "Invalid email.";
      } 

      if (errors) errorCount  = 1;

      return { ...field, errors };
    }) as T;

    return { validatedFields, errors: errorCount };
  } catch (err) {
    throw String(err);
  }
};
  

Typescript playground