0

I'm pretty much new to Typescript world and am converting one of the first React applications.

Here I'm trying to set custom messages on input validation by using event.target.setCustomValidity but I'm getting the 'Illegal invocation' error when the setCustomValidity function gets triggered.

Here there is the sandbox; To trigger the error it's just required to update the first input.

Debugging the error, I'd think the issue stands on the declared type of event.target set as HTMLInputElement, or on the declared type of event set as a React.SyntheticEvent; Perhaps I'm using the wrong types for the event which doesn't always have a setCustomValidity function?

To premise the code used on the sandbox works perfectly fine without Typescript

Any suggestion is highly appreciated, thank you in advance for your help!

Components code

App.tsx:

import React, { useState } from "react";
import Input from "./Input";
import "./styles.css";

export default function App() {
  // Check if value entered is a valid email. Ref: http://emailregex.com/
  const emailPattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;

  const initialForm = {
    username: "",
    password: ""
  };
  const [form, setForm] = useState(initialForm);

  // Listen to form inputs and updates form state respectively
  const handleChange = (event: React.SyntheticEvent) => {
    const {
      name,
      value,
      type,
      setCustomValidity
    } = event.target as HTMLInputElement;
    setForm({ ...form, [name]: value });

    if (type === "email" && !emailPattern.test(value)) {
      setCustomValidity("Please select a valid email address.");
    } else {
      setCustomValidity("");
    }
  };

  return (
    <div className="App">
      <Input
        id="username"
        type="string"
        name="username"
        label="username"
        value={form["username"]}
        onChange={handleChange}
        required={true}
      />
      <Input
        id="password"
        type="password"
        name="password"
        label="password"
        value={form["password"]}
        onChange={handleChange}
        required={true}
      />
    </div>
  );
}

Input.tsx:

import React from "react";

type PropsType = {
  id: string;
  type: string;
  name: string;
  value: string;
  label: string;
  onChange: (event: React.SyntheticEvent) => void;
  required: boolean;
};

/**
 * Reusable standard Input component.
 * @param {Object} props - Props passed from parent component containing input attributes.
 * @return - Controlled input element.
 */
const Input: React.FC<PropsType> = ({
  id,
  type,
  label,
  onChange,
  required,
  ...rest
}: PropsType) => (
  <div className="input">
    <input
      id={id}
      type={type}
      placeholder=""
      {...rest}
      onChange={(event) => onChange(event)}
      minLength={type === "password" ? 8 : undefined}
    />
    <label htmlFor={id}>
      {label}
      {required && " *"}
    </label>
  </div>
);

export default Input;
ale917k
  • 1,494
  • 7
  • 18
  • 37
  • Type declarations do not impact runtime behavior. – Aluan Haddad Jan 04 '21 at 16:52
  • 2
    The problem is the use of destructuring to get the `setCustomValidity` function. If you replace it with `event.target.setCustomValidity("");` it works fine. If so, this is likely a duplicate of [Why are certain function calls termed “illegal invocations” in JavaScript?](https://stackoverflow.com/q/10743596/215552) – Heretic Monkey Jan 04 '21 at 17:05
  • @HereticMonkey Thank you for pointing out; That was what I first implemented, but I had to update as I was getting ts errors: Property 'setCustomValidity' does not exist on type 'EventTarget'.ts(2339) - How could I go around that? – ale917k Jan 04 '21 at 17:12

1 Answers1

2

As @Heretic Monkey pointed out, you should use event.target.setCustomValidity rather than destructuring the method to avoid losing the context of the function when it's called. See Why are certain function calls termed "illegal invocations" in JavaScript?.

Then, to properly type the event you may want to start by changing the onChange typing in Input.tsx.

// src/Input.tsx
type PropsType = {
    //...
    onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
};

Then update your handleChange callback accordingly in App.tsx.

// src/App.tsx
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    //...
    if (type === "email" && !emailPattern.test(value)) {
        event.target.setCustomValidity("Please select a valid email address.");
    } else {
        event.target.setCustomValidity("");
    }
}
juliomalves
  • 42,130
  • 20
  • 150
  • 146
  • any reason why we should like this `event.target.setCustomValidity` instead of destructuring? – Rajiv Sep 04 '22 at 13:20
  • 1
    @Rajiv There is, to avoid losing the context of the function when it's called. See [Why are certain function calls termed "illegal invocations" in JavaScript?](https://stackoverflow.com/questions/10743596/why-are-certain-function-calls-termed-illegal-invocations-in-javascript). I've updated my answer with the clarification. – juliomalves Sep 04 '22 at 14:52