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
}
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
*/
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"
],
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,
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>
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
};
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 (8)
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.
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.
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.
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.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!
My pleasure! Glad it's fixed ๐
I do have dom.iterable in my tsconfig, nevertheless tsc still says "entries()" does not exist. Worse, if I use Babel to compile anyway - FormData(form) does not contain the values the form has.
I tried just using Parcel, and it complains that I have a file ".mp3" that is invalid version - but I long ago took that file out since it's not relevant to the current stage I'm in and I didn't want to hassle with it. So it's not even there! Why does parcel complain? It 's not in any config files, although it IS mentioned in the ts file (just as a string, though).
Thank you so much man