skip to Main Content

In my handleChange method I’ve tried to combine checkbox with other input/select elements:

handleChange = (
    event:
      | ChangeEvent<HTMLTextAreaElement>
      | ChangeEvent<HTMLInputElement>
      | ChangeEvent<HTMLSelectElement>
  ) => {
    this.setState({
      [event.target.name]:
        event.target.type == "checkbox"?
          event.target.checked:event.target.value,
    } as unknown as Pick<IFormState, keyof IFormState>);
  };

But encountered with this error: Property 'checked' does not exist on type '(EventTarget & HTMLTextAreaElement) | (EventTarget & HTMLInputElement) | (EventTarget & HTMLSelectElement)'. Property 'checked' does not exist on type 'EventTarget & HTMLTextAreaElement'.ts(2339)

Why it’s combine two entities, like EventTarget & HTMLTextAreaElement and if it possible to resolve it here?

Now I have separate handler for checkbox which is working properly:

handleCheckboxChange = (event: ChangeEvent<HTMLInputElement>) => {
    this.setState({
      [event.target.name]: event.target.checked,
    } as unknown as Pick<IFormState, keyof IFormState>);
  };

2

Answers


  1. Chosen as BEST ANSWER

    I've just implemented type assertion principle here for event.target as HTMLInputElement:

    handleChange = (
        event:
          | ChangeEvent<HTMLTextAreaElement>
          | ChangeEvent<HTMLInputElement>
          | ChangeEvent<HTMLSelectElement>
      ) => {
        this.setState({
          [event.target.name]:
            event.target.type == "checkbox"
              ? (event.target as HTMLInputElement).checked
              : event.target.value,
        } as unknown as Pick<IFormState, keyof IFormState>);
      };
    

  2. This is because the property type does not discriminate between the different HTMLElement types: you know that the value of type for checkbox elements is "checkbox" at runtime, but at compile time – or writing time if you prefer – the compiler doesn’t know anything about that.

    So, when trying to access the checked property, the compiler assumes that it must be present in every type of the union that can be passed to the method. And don’t find it in two of them.

    It all comes down to the way the various HTMLElement types have been written. If you look at them, you’ll see that they don’t declare the property type in a way that allows the compiler to discriminate between them:

    interface HTMLInputElement extends HTMLElement, PopoverInvokerElement {
        // ...
        type: string;
    }
    

    type is of type string.

    Would it have been written this way, it would allow the compiler to discriminate:

    interface HTMLInputElement extends HTMLElement, PopoverInvokerElement {
        // ...
        type: "input";
    }
    

    And of course, ideally, there should be a type for the checkbox element, establishing that the type is "checkbox":

    interface HTMLCheckboxElement extends HTMLInputElement {
        // ...
        type: "checkbox";
    }
    

    But this type doesn’t exist, at least in the official type declaration that my IDE depends on.

    Anyway, the behaviour that you encounter is perfectly normal. I recommend that you write your own discriminated types to overcome this issue – it is easy and actually kinda fun.

    EDIT: Something like that:

    export interface MyHTMLElement<Type extends string> extends Omit<HTMLElement, "type"> {
        type: Type;
    }
    
    export interface MyInputHTMLElement<Type extends string = "input"> extends MyHTMLElement<Type> {
        foo: number;
    }
    
    export interface MyCheckboxHTMLElement extends MyInputHTMLElement<"checkbox"> {
        bar: string;
    }
    
    const doSomething = (
        element: MyInputHTMLElement | MyCheckboxHTMLElement
    ) => {
        if (element.type === "checkbox") {
            console.log(element.bar);
        }
        else {
            console.log(element.foo);
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search