DEV Community

Ben Lovy
Ben Lovy

Posted on

FormData in TypeScript

I'm trying to get in the habit of writing a post about anything that takes me more than, say, 15 minutes of trial and error and Google to get right. Chances are I'm not the first nor the last to hit any particular roadblock.

This is both a tutorial and a request for help!

edit: @foresthoffman provided the help I needed! I've updated the post to include the fix.

I'm working on a small toy app that needs a few controls to specify how to draw something to the canvas. A <form> seems like the natural choice for this, but using the data from TypeScript proved a little bit tricky.

The MDN page on FormData suggests using syntax like the following:

const form = document.querySelector('form')
const data = new FormData(form);

for (const pair of data) {
    // cool stuff
}
// OR
for (const pair of data.entries()) {
    // cool stuff
}
Enter fullscreen mode Exit fullscreen mode

I had no luck with these. Using for..of causes TypeScript to complain that data is not an iterator (I think it is, or at least in JS it can be used like one with for...of), or that data has no such property entries. This makes a little more sense - it doesn't yet in all environments. I tried tweaking my tsconfig.json to target esnext but that didn't do it, and I'd rather keep that set to es5 anyway. Switching to use for..in on data does what you'd expect, really - it enumerates all of the methods available on data:

const data = new FormData(form);
for (const entry in data) {
  console.log(entry);
}
/*
get
getAll
has
set
entries
keys 
values
forEach
*/
Enter fullscreen mode Exit fullscreen mode

Neat, I guess, but not what I'm looking for! Frustratingly, entries appears. Why can I not use it?

It turns out the fix for this is subtle - you need to specifically tell TypeScript you're going to be using this method by adding dom.iterable to your tsconfig.json - it's not automatically brought in with "dom":

"lib": [
  "dom",
  "dom.iterable",
  "esnext"
],
Enter fullscreen mode Exit fullscreen mode

Now you can for (let entry of data.entries()) to your heart's content! That's still not as concise as it could be, though - in JavaScript you can just write (let entry of data). We can allow this pattern in TypeScript by adding one more line to tsconfig.json:

"downlevelIteration": true,    
Enter fullscreen mode Exit fullscreen mode

This compiler option "provide[s] full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'." Now our code can match the JS exactly!

I'm leaving my workaround for posterity, because in some simple cases I'd prefer to skip the iterator and do it this way anyway. It simply doesn't iterate at all, it looks for what it needs. As an example, here's part of the form in question:

<form>
        <fieldset>
            <legend>Choices</legend>

            <input type="radio" name="choice" id="choice1" value="choice1" checked>
            <label for="choice1">Choice 1</label>

            <input type="radio" name="choice" id="choice2" value="choice2">
            <label for="choice2">Choice 2</label>
        </fieldset>
        <button type="submit">Do The Thing!</button>
</form>
Enter fullscreen mode Exit fullscreen mode

To get at the user's choice, I'm using code like this:

const form = document.querySelector('form')!;

form.onsubmit = (_) => {
  const data = new FormData(form);
  const choice = data.get('choice') as string;
  doCoolThing(choice);
  return false; // prevent reload
};
Enter fullscreen mode Exit fullscreen mode

A few TypeScript-y things to note - I'm using the ! operator at the end of the querySelector call. This is the non-null assertion operator - querySelector returns a value of type Element | null. I prefer to keep strictNullChecks on, so TS doesn't enjoy me trying to operate on form as if it were an element - this operator tells the compiler that I promise there will be a form to select and it won't return null.

Also, FormData.get() returns a value of type string | File | null. This is another case where I've quite literally just written the form myself - I know it's gonna be a string. I'm using as to cast to the proper type.

Finally, I return false to prevent the page from actually reloading - the re-draw to the canvas happens inside doCoolThing, and if the page reloads it'll disappear along with the form data! I'm not sending anything to a server, just using the user input locally on the page.

This does do the trick - I can just grab the the form data I want one at a time without using the iterator to configure the output.

Top comments (6)

Collapse
 
foresthoffman profile image
Forest Hoffman • Edited

Could you use the FormData.entries() method? developer.mozilla.org/en-US/docs/W...

It appears to return key-value pairs that you can iterate over.

e.g.

const data = new FormData(form);
const entries = data.entries();
for (let entry of entries) {
  const key = entry[0];
  const val = entry[1];
  console.log(key, val);
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
deciduously profile image
Ben Lovy • Edited

That's what I couldn't get working. The above code was not accepted by the TS compiler, complaining that there is no such property entries.

edit: when I say not accepted, it does compile because it's TypeScript. It flags the line with an error, and the code block simply does not execute. Nothing gets logged to the console.

Collapse
 
foresthoffman profile image
Forest Hoffman

Hmm, that is odd. I saw a TS issue related to this, github.com/Microsoft/TypeScript/is.... Someone suggested using Array.from() to mitigate this.

e.g.

form.onsubmit = (event) => {
    const data = new FormData(form);
    const pairs = Array.from(data.entries());
    for (let pair of pairs) {
        console.log(pair);
    }
};
Thread Thread
 
deciduously profile image
Ben Lovy

Interesting - thank you for the issue link, I'll give it a whirl when I get back to my computer. I missed that one when searching it myself.

In that conversation, though, they mention needing to use a target < es2015 - I did try doing that instead to no avail. If Array.from() solves the issue, though, it's a small price to pay, I don't think it's any less readable.

Thread Thread
 
deciduously profile image
Ben Lovy

Ah - another comment in that thread had the fix. You need to add "dom.iterable" to your libs in tsconfig - I'll update the post. Thank you thank you!

Thread Thread
 
foresthoffman profile image
Forest Hoffman

My pleasure! Glad it's fixed 😀