'How to work with form elements in typescript

I'd like to access form elements via myForm.elements, and then access each element by it's name, for example, myForm.elements.month. Typescript doesn't like this b/c it doesn't know that form.elements contains a property of month. I thought, let's create an interface! So I did, (see code below), but I'm getting this typescript error: Neither type 'HTMLCollection' nor type 'FormElements' is assignable to the other

Here's the code I'm working with:

interface FormElements {
    day: HTMLInputElement;
    month: HTMLInputElement;
    year: HTMLInputElement;
}

class BirthdateInput {
    constructor(form: HTMLFormElement) {
        var elements: FormElements = <FormElements> form.elements; // error here

        this.day = elements.day;
        this.month = elements.month;
        this.year = elements.year;
    }
}

Any ideas on how to better cast my form.elements object so typescript won't complain?



Solution 1:[1]

Best way would be to write it like this:

// Note 'extends' clause here
interface FormElements extends HTMLFormElement {
    day: HTMLInputElement;
    month: HTMLInputElement;
    year: HTMLInputElement;
}

class BirthdateInput {
    constructor(form: HTMLFormElement) {
        var elements: FormElements = <FormElements> form.elements; // OK
        // ...

Solution 2:[2]

Turns out adding an extends clause fixes it:

interface FormElements extends HTMLCollection {
    day: HTMLInputElement;
    month: HTMLInputElement;
    year: HTMLInputElement;
}

Solution 3:[3]

Origin answer

Create an interface extending HTMLFormControlsCollection or HTMLCollection and add there your inputs

interface FormElements extends HTMLFormControlsCollection {
    day: HTMLInputElement
    month: HTMLInputElement
    year: HTMLInputElement
}

Eventually, for example, getting this

interface FormElements extends HTMLFormControlsCollection {
    day: HTMLInputElement
    month: HTMLInputElement
    year: HTMLInputElement
}
function onSearch(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const elements = event.currentTarget.elements as FormElements

    // ...
  }

However

You can create a helper so you don't need to write an interface for each form every time

type FormElements<U extends string> = HTMLFormControlsCollection & Record<U, HTMLInputElement>

And use it like this

const elements = event.currentTarget.elements as FormElements<"id" | "name" | "type" | "amount">

Eventually, for example, getting this

function onSearch(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const elements = event.currentTarget.elements as FormElements<"id" | "name" | "type" | "amount">

    // ...
  }

Solution 4:[4]

Whether right or wrong, I have found that you can also do something like this:

interface FormElements {
    day: HTMLInputElement;
    month: HTMLInputElement;
    year: HTMLInputElement;
}

class BirthdateInput {
    constructor(form: HTMLFormElement) {
        var elements: FormElements = <FormElements>(<any> form.elements); 

        this.day = elements.day;
        this.month = elements.month;
        this.year = elements.year;
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ryan Cavanaugh
Solution 2 lizlux
Solution 3 FrameMuse
Solution 4 Alex Dresko