Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow constructing URLSearchParams directly from FormData #30584

Closed
thatbudakguy opened this issue Mar 25, 2019 · 27 comments
Closed

allow constructing URLSearchParams directly from FormData #30584

thatbudakguy opened this issue Mar 25, 2019 · 27 comments
Labels
Bug A bug in TypeScript Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript
Milestone

Comments

@thatbudakguy
Copy link

TypeScript Version: 3.4.0-dev.20190323

Search Terms:
URLSearchParams, FormData

Code

let form = document.createElement('form')
console.log(new URLSearchParams(new FormData(form)))

Expected behavior:
compilation succeeds and an (empty) URLSearchParams instance is constructed using the provided FormData. The above code runs without issue on Chrome 73 and Firefox 67 for me.

Actual behavior:
compilation fails because URLSearchParams doesn't explicitly allow an argument of type FormData as input to the constructor.

Argument of type 'FormData' is not assignable to parameter of type 'string | Record<string, string> | URLSearchParams | string[][]'.
  Property 'sort' is missing in type 'FormData' but required in type 'URLSearchParams'.

Playground Link:
link

Related Issues:
#12517
#15338

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript labels Mar 25, 2019
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Mar 25, 2019
@thatbudakguy
Copy link
Author

If all that's required for this is to add | FormData to the constructor signature, I'm happy to PR.

@saschanaz
Copy link
Contributor

#19806 covers this.

@leonstafford
Copy link

@thatbudakguy - in the meantime, do you have a workaround for this?

@thatbudakguy
Copy link
Author

@leonstafford my current workaround is just // @ts-ignore, sadly.

@raythurnvoid
Copy link

Another workaround is: new URLSearchParams(new FormData(event.currentTarget) as any)

@JirkaDellOro
Copy link

Is there a deadline for this bug to be fixed?

@JirkaDellOro
Copy link

No? Yes? When?

@Spongman
Copy link

any news?

sstephenson added a commit to hotwired/turbo that referenced this issue Jan 29, 2021
Although Chrome, Firefox, and Safari all seem to implement support for `new URLSearchParams(formData)`, the [spec][1] doesn’t mention it, the IDL and [TypeScript][2] don’t have typings for it, and searching suggests it’s a recent addition.

[1]: https://url.spec.whatwg.org/#dom-urlsearchparams-urlsearchparams
[2]: microsoft/TypeScript#30584
@PaperStrike
Copy link

PaperStrike commented Aug 1, 2021

I don't think it will be "fixed" as FormData may contain File while URLSearchParams only accepts String.

That code runs successfully in browsers by the implicit conversions in JS. Explicit matched types are needed in TS. Try to filter out the Files, or convert them to Strings. For example,

// For files, use their names
const convertedFormEntries = Array.from(
  new FormData(),
  ([key, value]) => (
    [key, typeof value === 'string' ? value : value.name]
  ),
);
const searchParams = new URLSearchParams(convertedFormEntries);

You can see the error more clearly in this TS playground.

@JirkaDellOro
Copy link

Well, MDN writes that the constructor accepts one the following:

  • A USVString, which will be parsed from application/x-www-form-urlencoded format. A leading '?' character is ignored.
  • A literal sequence of name-value string pairs, or any object — such as a FormData object — with an iterator that produces a sequence of string pairs. Note that that File entries will be serialized as [object File] rather than as their filename (as they would in an application/x-www-form-urlencoded form).
  • A record of USVString keys and USVString values.

Since FormData is explicitly mentioned, TypeScript should gladly compile it and the definition file for the dom should include the mentioned types in the constructors signature. The fact that file won't show a filename but [object File] is a logical problem at runtime, not at compiletime.

https://jsfiddle.net/3ng6qkL1/

@saschanaz
Copy link
Contributor

saschanaz commented Aug 1, 2021

The fact that file won't show a filename but [object File] is a logical problem at runtime, not at compiletime.

And TypeScript is all for preventing such logical typing problems.

Maybe FormData could be made generic so that this can work:

const data = new FormData<string>();
console.log(new URLSearchParams(data));

I'm not sure new FormData<string>(formElement) makes sense since the element may include non-strings.

@JirkaDellOro
Copy link

And TypeScript is all for preventing such logical typing problems.

True, On the other hand, TypeScript should not prevent standard functionality to work as intended.

@PaperStrike
Copy link

PaperStrike commented Aug 1, 2021

MDN isn't the standard, strictly speaking.

The file name conversion is defined in form entry list to name-value pairs conversion, HTML Standard. Click on the second bold sentence, it shows where it is used.

The note about [object File] isn't mentioned anywhere in steps to initialize a URLSearchParams, URL Standard.
It happens by implicit conversions.

@JirkaDellOro
Copy link

Now let me perform an elegant 180 after digging into it. This issue should not be labeled bug but works as intended and praised as a feature of TypeScript.

@thatbudakguy
Copy link
Author

closing, since as others have mentioned it's not a bug for TypeScript to avoid coercing File despite this being common behavior elsewhere. thread offers several workarounds for anyone still looking to do this.

@sacru2red
Copy link

sacru2red commented Jan 3, 2023

Negated types feature could clear it. in future
any not File

or
FormData methods should take a Generic #43797

@lightyaer
Copy link

Another workaround:

  const formData = new FormData();
  formData.set('something', "something");

  const searchParams = new URLSearchParams(
    formData as unknown as Record<string, string>,
  ).toString();

if value is an object, then JSON.stringify(object);

@thoughtsunificator
Copy link

thoughtsunificator commented Oct 29, 2024

By reading the FormData spec :

Image

You can see that FormData is an iterable whose values are a sequence that contains a both USVString and FormDataEntryValue (which itself can be a File or USVString) as values, and according to the spec:

Objects implementing an interface that is declared to be iterable support being iterated over to obtain a sequence of values.

FormData is an iterable according to the same spec.

And File is implementing Serializable, so for the serialization part, it says that :

The resulting data serialized into serialized must be independent of any realm.

The spec also says that URLSearchParams is an iterable<USVString, USVString>, it expects a sequence of two strings.

It would then make sense for URLSearchParams to try to serialize what can be, it's the whole point of serialization in the first place.

Hence, the [object File].

An Error, being a Serializable, will also be serialized:

const form = document.createElement('form')
console.log(new URLSearchParams([[new Error("foo"),"bar"]]).toString())
// Output "Error%3A+foo=bar"

Which means, that FormData is a valid init parameter according to the URLSearchParams IDL:

Image

@ITenthusiasm
Copy link

@thatbudakguy any thoughts about the above? If you agree, would you up for re-opening this issue?

@PaperStrike
Copy link

@thoughtsunificator

  1. Number, undefined, null, File, FileList, Error, the array and object combinations of all these and many more are serializable. You don't need TypeScript if you consider them all as strings, just type in @ts-expect-error.

  2. For objects annotated with [Serializable] attribute specifically, they have detailed desired serialization and deserialization steps that should be taken and let's take File as an example

    File objects are serializable objects. Their serialization steps, given value and serialized, are:

    1. Set serialized.[[SnapshotState]] to value’s snapshot state.
    2. Set serialized.[[ByteSequence]] to value’s underlying byte sequence.
    3. Set serialized.[[Name]] to the value of value’s name attribute.
    4. Set serialized.[[LastModified]] to the value of value’s lastModified attribute.

    I don't see how this will make [object File].

@thatbudakguy
Copy link
Author

@thatbudakguy any thoughts about the above? If you agree, would you up for re-opening this issue?

I'm not convinced this needs to be reopened. When I opened it, I just wanted to quickly convert a form's data into query parameters, but had forgotten that forms can include File. That seems like exactly the situation where TS is helpful: to notify you of edge cases you might otherwise miss.

If you're in the same situation, use the solution in #30584 (comment) with a comment about handling File, even if your app will never actually receive a File. Maybe it'll help someone else who comes after you.

@ITenthusiasm
Copy link

That seems like exactly the situation where TS is helpful: to notify you of edge cases you might otherwise miss.

I'm not sure that this is true. There are some instances where TypeScript really should not intervene because it starts overstepping by trying to predict runtime behaviors which are intrinsically unpredictable. This is why, for example, JSON.parse() returns any instead of unknown. The unknown type is technically more "type safe", but it is far less convenient or reasonable. And the real bugs resulting from JSON.parse() would be better caught during runtime and/or with tests.

Similarly, regarding FormData, this is a runtime issue. Problems with FormData likely won't be caught with any kind of type system. They'll be caught when submitting data to the server and seeing the response. So I still think it's worth re-opening this issue.

If you're in the same situation, use the solution in #30584 (comment)

The comment that you linked to has negative impacts on performance by creating throw-away memory. Moreover, although I haven't tested it yet, that code might produce bugs for scenarios where a given key has multiple values (e.g., <select multiple>). (I know, for instance, that Object.fromEntries(formData) definitely produces bugs in that scenario.) TypeScript is supposed to be something helpful, but developers shouldn't feel burdened to complicate or slow-down their code simply for a compiler. In a sense, that defeats the point of compilers.

Is there a better solution that you would propose (besides any or @ts-expect-error)? If not, then -- again -- it seems this issue isn't truly completed and should be re-opened, no?

@thatbudakguy
Copy link
Author

#43797 seems like a wholly valid solution.

@ITenthusiasm
Copy link

Seems limited but I'll investigate more thoroughly

@PaperStrike
Copy link

PaperStrike commented Oct 30, 2024

There are some instances where TypeScript really should not intervene because it starts overstepping by trying to predict runtime behaviors which are intrinsically unpredictable.

Yes, that's why TypeScript has as, ! (the one to exclude nullability), generics, overloads, @ts-ignore, @ts-expect-error and of course any for some totally unpredictables.

This is why, for example, JSON.parse() returns any instead of unknown.

I doubt if TypeScript would pick any if they have unknown since v1.0. Search JSON.parse unknown in this repo and there's so many asking making it unknown. A quick glance over the maintainers' responses look like they relate this change to some bigger change and consider the big change too breaking so they still left it under consideration.

Problems with FormData likely won't be caught with any kind of type system.

TypeScript caught mine and maybe also Budak's problem. I was writing a library that handles forms with possible File fields and confused about the error as you know URLSearchParams and FormData look so perfectly matched.

Moreover, although I haven't tested it yet, that code might produce bugs for scenarios where a given key has multiple values (e.g., <select multiple>).

It won't, just give it a try, it works fine.

TypeScript is supposed to be something helpful, but developers shouldn't feel burdened to complicate or slow-down their code simply for a compiler. In a sense, that defeats the point of compilers.

I agree that it's hard to relate with File in the first place by the error message (maybe caused by microsoft/TypeScript-DOM-lib-generator#741 that the current constructor don't allow string sequence iterables too). TypeScript should somehow improve that but it would be in different issue(s).

For the slow down I used to choose as (as unknown as string[][]) or @ts-expect-error randomly whenever there's no possible File and leave a comment why the cast / expect is there. Recently I rarely deal with FormData directly and I would wrap it to a universal utility that ought to handle any FormData 2 URLSearchParams conversion well if I need to.

Is there a better solution that you would propose (besides any or @ts-expect-error)? If not, then -- again -- it seems this issue isn't truly completed and should be re-opened, no?

as, #43797 generic.

@ITenthusiasm
Copy link

Yes, that's why TypeScript has as, ! (the one to exclude nullability), generics, overloads, @ts-ignore, @ts-expect-error and of course any for some totally unpredictables.

Those utilities do solve a number of problems. But I'm not sure I'd say they're intended for the concern that I raised earlier.

I doubt if TypeScript would pick any if they have unknown since v1.0.

I'm basing this on a convo with orta after axios caught flack for changing any to unknown. A mind change may have happened since that incident, but I don't know that unknown is the obvious solution; not everyone wants it.

TypeScript caught mine and maybe also Budak's problem.

That does not mean all problems can be caught (e.g., explicit application/x-www-form-urlencoded w/ [type=file] fields). There's a decent set that still has to be caught at runtime (or by foreknowledge). And to be fair, it's worth questioning whether TS really caught the problem or forced you to research/pursue an alternative.

It won't, just give it a try, it works fine.

You are right. 👍🏾 This was an invalid assumption. Unfortunately, there's still the performance aspect.

For the slow down I used to choose as (as unknown as string[][]) or @ts-expect-error randomly whenever there's no possible File and leave a comment why the cast / expect is there.

It's this dancing that I don't particularly favor (when it's unnecessary). I understand that you do not frequently use the FormData object directly anymore, but that doesn't mean other developers don't.

as, #43797 generic.

My any comment was referencing type casting in general. (as any is faster than as unknown as *, and they're both effectively the same hack.) That other GH issue was referenced earlier, yes.

@sandersn
Copy link
Member

I'm not convinced this is worth re-opening. The spec snippets that @thoughtsunificator posted make it clear that Typescript should regard URLSearchParams's constructor as taking Record<string, string> and FormData as Record<string, string | File> .
And Record<string, string | File> isn't assignable to Record<string, string>`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Domain: lib.d.ts The issue relates to the different libraries shipped with TypeScript
Projects
None yet
Development

No branches or pull requests