How to type a React form onSubmit handler
by Kent C. Dodds

Here's a form in JSX
:
function UsernameForm({onSubmitUsername}) {function handleSubmit(event) {event.preventDefault()onSubmitUsername(event.currentTarget.elements.usernameInput.value)}return (<form onSubmit={handleSubmit}><div><label htmlFor="usernameInput">Username:</label><input id="usernameInput" type="text" /></div><button type="submit">Submit</button></form>)}
Let's type that handleSubmit
function. Here's how some people do it (copying
this approach from some blog posts and "semi-official guides" I've seen):
function handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {event.preventDefault()const form = event.currentTargetconst formElements = form.elements as typeof form.elements & {usernameInput: {value: string}}onSubmitUsername(formElements.usernameInput.value)}
The reason we have to have the as
there is because TypeScript isn't quite
smart enough to know what elements we're rendering in our form, so we have to
use the type cast which I'm not a fan of, but you gotta ship right?
My first improvement to this is to change that usernameInput
type:
- usernameInput: {value: string}+ usernameInput: HTMLInputElement
Definitely take advantage of the type it actually is rather than just cherry-picking the values you need.
The second improvement is:
- function handleSubmit(event: React.SyntheticEvent<HTMLFormElement>) {+ function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
Incidentally, at the time of this writing, there's no substantive difference in those types, but I prefer to be more clear and accurate with the name of the type, so that's what we're going to go with.
So here we are now:
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {event.preventDefault()const form = event.currentTargetconst formElements = form.elements as typeof form.elements & {usernameInput: HTMLInputElement}onSubmitUsername(formElements.usernameInput.value)}
But even with those changes, I'm not a fan of this for three reasons:
- It's a bit verbose and distracts from the main purpose of the function
- If I need to use that
form
anywhere else, I'll either have to duplicate the code or extract the type and cast it everywhere I use the form. - I don't like seeing
as
in my code because it is a signal that I'm telling the TypeScript compiler to pipe down (be less helpful). So I try to avoid it when possible. And it is possible!
Keep in mind that we're the ones telling TypeScript what that
event.currentTarget
type is. We tell TypeScript when we specify the type for
our event
. Right now that's set to React.FormEvent<HTMLFormElement>
. So
we're telling TypeScript that event.currentTarget
is an HTMLFormElement
but
then we immediately tell TypeScript that this isn't quite right by using as
.
What if instead we just tell TypeScript more accurately what it is at the start?
Yeah, let's do that.
interface FormElements extends HTMLFormControlsCollection {usernameInput: HTMLInputElement}interface UsernameFormElement extends HTMLFormElement {// now we can override the elements type to be an HTMLFormControlsCollection// of our own design...readonly elements: FormElements}
So in reality, our form
is an HTMLFormElement
with some known elements
. So
we extend HTMLFormElement
and override the elements
to have the elements we
want it to. The HTMLFormElement['elements']
type is a
HTMLFormControlsCollection
, so make our own version of that interface as well.
With that, now we can update our type and get rid of all the type casting!
Here's the whole thing altogether:
import * as React from 'react'interface FormElements extends HTMLFormControlsCollection {usernameInput: HTMLInputElement}interface UsernameFormElement extends HTMLFormElement {readonly elements: FormElements}function UsernameForm({onSubmitUsername,}: {onSubmitUsername: (username: string) => void}) {function handleSubmit(event: React.FormEvent<UsernameFormElement>) {event.preventDefault()onSubmitUsername(event.currentTarget.elements.usernameInput.value)}return (<form onSubmit={handleSubmit}><div><label htmlFor="usernameInput">Username:</label><input id="usernameInput" type="text" /></div><button type="submit">Submit</button></form>)}
So now:
- the type information is removed from the
handleSubmit
code, allowing us to focus on what the function is doing. - If we need to use this
form
somewhere else, we can give it theUsernameFormElement
type and get all the type help we need. - We don't have to use a type cast, so TypeScript can be more useful for us.
I hope that's helpful to you!