TypeScript
TypeScript copied to clipboard
FormData methods should take a Generic
Suggestion
🔍 Search Terms
Form Data, .entries()
✅ Viability Checklist
My suggestion meets these guidelines:
- [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
- [x] This wouldn't change the runtime behavior of existing JavaScript code
- [x] This could be implemented without emitting different JS based on the types of the expressions
- [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- [x] This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
FormData entries() method, and perhaps .keys() and .values() should take a generic.
Right now it returns FormDataEntry value and is difficult type type the returned value of that Iterable.
📃 Motivating Example
Example of the problem here:
https://www.typescriptlang.org/play?strictNullChecks=false&target=6#code/PTAEEEGdIVwWwKagIagGYHsBOdQBsBLAayQBcALAyUAgO1AqQBEB5AWQBpQAjGU0AO4EK+ZNwR5qCUgGMAdHIBQMjLUj9MOUAF5QwAFShypOHlD6wAAwA8muAD5FoUNboAHPgwCebhNoBE6lh0AOb+oLTIiAFoBFjq4QQAJjFxCaAAbsh4MH7+AgiQ-o7OrrQe-KQ+eUGh4ZHR-njI6ckBzelZOXncGEWO1sB29pYA3IqKIKAAKuQI9HSgcF4zAMoAXBN0pAhYaMgySAAKu5CqoADeTuhppOugtbQh484ddw+kwU-jAL4TaDBaDJSARzpBkF4ABIEAAUvniqnuJwRtAAlJdrlhpDAsPRLJCECsACQXeFnWhyWLxUg-MaKP7KVTqdDYOAAUTMuiSGBk8HmpDkAEdclgvKsJAhgdgYQByOwy1HjFRqDSspjIUioXS0BACUAAMTVGuQMLsHMVkzAAGFVBldpVyBqHjAZCQsAwMCgFjssGI8AhGSrQEljQBJH1+pC6OzqzVyfnBQowi1TG20O1YB1O4S7SMer2gDDcABWkoFlpmc1AbiwRf9uBUiGocyxXHIGD13NAoaWyBIgkKKFAyPJ3l8AH5QIHmQVqLoWCWy5Ta+zaJ8CEmQ5rw7nuP6LeCobDZxaK7MqMGMPNIDL+AJsERp3fCgAmHSgBel4HLjCr9ebsMIz3BB0RaYdTlUcYz0oahuUKLheH4C9hELIgIUncUEDgahYkgcgvDkKdlRnQoAGZ30-Jc0BXNk10TSAYS3ZAd19YDQOoQEiFoDt6DAkdIMUQ9oRhWdSNPCsWEYd1ZgwGAQmMSB7grABGQjzykAAPKI3H9GhqEgAg4B0hAuB7FQYDwJJe1oGBsjwFYZCxDUyCrIsv1IBDPAoOgiBQXpPGaLAQl2QtF2BSAgA
Feeding the .entries() Iterable into Object.fromEntries(), gives us a type of { [k: string]: FormDataEntryValue; }
, and has no overlap between the acutal data (Person
in my case) that is supposed to come back.
So perhaps something like formData.entries<Person>()
or formData.entries<IterableIterator<Person>>()
?
I just know it's really hard to convert the result into a type. And the solutions are either as unknown as Person
or a Type Guard, which makes me manually type each property.
💻 Use Cases
See above link example
How about making FormData itself generic?
So we can do something like this:
const formData: FormData<Person> = new FormData(formEl);
I am looking forward to it, it is very useful to me.
Here is my solution: define a custom FormData
interface IRuntimeForm {
[key: string]: any;
}
export class RuntimeForm<T extends IRuntimeForm> {
constructor(private _form: T) {}
public formData(): FormData {
const form = new FormData();
for (const key in this._form) {
if (this._form[key] !== undefined) {
form.append(key, this._form[key]);
}
}
return form;
}
}
new RuntimeForm<{ foo: string }>({ foo: "bar" })
How about making FormData itself generic?
So we can do something like this:
const formData: FormData<Person> = new FormData(formEl);
yes that'd be great
Here is my solution: define a custom FormData
interface IRuntimeForm { [key: string]: any; } export class RuntimeForm<T extends IRuntimeForm> { constructor(private _form: T) {} public formData(): FormData { const form = new FormData(); for (const key in this._form) { if (this._form[key] !== undefined) { form.append(key, this._form[key]); } } return form; } } new RuntimeForm<{ foo: string }>({ foo: "bar" })
thanks, nice temp solution. generic form data would be a great feature still.
export class RuntimeForm<T extends IRuntimeForm> {
constructor(private form: T) {}
public formData(): FormData {
const form = new FormData();
Object.keys(this.form).forEach((key) => {
if (this.form[key] !== undefined) {
form.append(key, this.form[key])
}
})
return form;
}
}
If anyone is using AirBnB rules and wants the above to work
Just wanted to note that this'd be a very welcome feature, especially as some newer frameworks like Remix put FormData front and center.
I started using FormData
a lot recently in a full-stack project and it's painful to use something not typed. This would be really handy.
- 1
+1 on this suggestion. Remix's reliance on FormData is making this much more important
+1
+1
+1
+1
+1
+1. I was trying to correct the type of a FormData.fromEntries and this was the first thing I tried, just to arrive here after looking around how to solve this.
+1, this would be very useful on Remix and new react router based projects
+1
+1, same using remix.
Yes please +1
+1
+1
Please stop with the +1's. Everyone subscribed to this issue gets a notification for it.
Use the emoji reactions on the first post instead.
Any news on this ?
This is what I ended up doing:
type TypedFormDataValue = FormDataEntryValue | Blob
/**
* Polyfill for FormData Generic
*
* {@link https://github.com/microsoft/TypeScript/issues/43797}
* {@link https://xhr.spec.whatwg.org/#interface-formdata}
*/
interface TypedFormData<T extends Record<string, TypedFormDataValue>> {
/**
* Appends a new value onto an existing key inside a FormData object, or adds the key if
* it does not already exist.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.append}
*/
append<K extends keyof T>(name: K, value: T[K], fileName?: string): void
/**
* Deletes a key/value pair from a FormData object.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.delete}
*/
delete(name: keyof T): void
/**
* Returns an iterator allowing to go through all key/value pairs contained in this object.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.entries}
*/
entries<K extends keyof T>(): IterableIterator<[K, T[K]]>
/**
* Returns the first value associated with a given key from within a FormData object.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.get}
*/
get<K extends keyof T>(name: K): T[K] | null
/**
* Returns an array of all the values associated with a given key from within a FormData.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.getall}
*/
getAll<K extends keyof T>(name: K): Array<T[K]>
/**
* Returns a boolean stating whether a FormData object contains a certain key.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.has}
*/
has(name: keyof T): boolean
/**
* Returns an iterator allowing to go through all keys of the key/value pairs contained in
* this object.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.keys}
*/
keys(): IterableIterator<keyof T>
/**
* Sets a new value for an existing key inside a FormData object, or adds the key/value
* if it does not already exist.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.set}
*/
set(name: keyof T, value: TypedFormDataValue, fileName?: string): void
/**
* Returns an iterator allowing to go through all values contained in this object.
*
* {@link https://developer.mozilla.org/en-US/docs/Web/API/FormData#formdata.values}
*/
values(): IterableIterator<T[keyof T]>
forEach<K extends keyof T>(
callbackfn: (value: T[K], key: K, parent: TypedFormData<T>) => void,
thisArg?: unknown,
): void
}
function getTypedFormData<T extends Record<string, TypedFormDataValue>>(
form?: HTMLFormElement | null,
): TypedFormData<T> {
return new FormData(form || undefined) as unknown as TypedFormData<T>
}
Pretty convenient, thanks @elving :)
I asked for a similar feature for Response
and Request
interfaces to accept generic type parameters: https://github.com/microsoft/TypeScript/issues/52777
Additional use case
A typed API client function would be a use case for this:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Options<Method> = {
method?: Method;
}
export async function fetchApi<Path extends string, Method extends HttpMethod>(
path: Path,
options: Options<Method> & {
formData: `${Method} ${Path}` extends `POST /exercise-checks`
? { id: string, file: File } // 👈 Could be FormData<{ id: string, file: File }>
: `${Method} ${Path}` extends `PUT /exercise-checks/${number}`
? { file: File } // 👈 Could be FormData<{ file: File }>
: never
}): Promise<
`${Method} ${Path}` extends `POST /exercise-checks`
? { id: string } | { errors: string[] }
: `${Method} ${Path}` extends `PUT /exercise-checks/${number}`
? { id: string } | { errors: string[] }
: never
> {
return '' as any;
}
const file = new File([''], '');
const goodFormData = { id: '1', file: file }; // 👈 Could be new FormData() + .append
const badFormData = { incorrectProperty: false }; // 👈 Could be new FormData() + .append
// ✅ No errors
await fetchApi('/exercise-checks', { method: 'POST', formData: goodFormData })
// ✅ No errors
await fetchApi('/exercise-checks/1', { method: 'PUT', formData: { file: file } })
// ✅ Errors, incorrect method
await fetchApi('/exercise-checks/1', { method: 'PUTzzzzz', formData: { file: file } })
// ✅ Error, incorrect path
await fetchApi('/xxxxx', { method: 'PUT', formData: { file: file } })
// ✅ Error, incorrect form data
await fetchApi('/exercise-checks/1', { method: 'POST', formData: badFormData })
+1
+1