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

FormData methods should take a Generic #43797

Open
5 tasks done
wesbos opened this issue Apr 23, 2021 · 34 comments
Open
5 tasks done

FormData methods should take a Generic #43797

wesbos opened this issue Apr 23, 2021 · 34 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@wesbos
Copy link

wesbos commented Apr 23, 2021

Suggestion

πŸ” Search Terms

Form Data, .entries()

βœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • 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

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Apr 23, 2021
@max-hk
Copy link

max-hk commented May 13, 2021

How about making FormData itself generic?

So we can do something like this:

const formData: FormData<Person> = new FormData(formEl);

@axetroy
Copy link

axetroy commented Aug 23, 2021

I am looking forward to it, it is very useful to me.

@axetroy
Copy link

axetroy commented Aug 23, 2021

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" })

@sambitevidev
Copy link

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

@cricketnest
Copy link

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.

@ZinkNotTheMetal
Copy link

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

@RyKilleen
Copy link

RyKilleen commented Mar 8, 2022

Just wanted to note that this'd be a very welcome feature, especially as some newer frameworks like Remix put FormData front and center.

@JohnCido
Copy link

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.

@chandlervdw
Copy link

  • 1

@filipemir
Copy link

+1 on this suggestion. Remix's reliance on FormData is making this much more important

@viktor-ulyankin
Copy link

+1

4 similar comments
@knenkne
Copy link

knenkne commented Aug 5, 2022

+1

@DigitalNaut
Copy link

+1

@altmshfkgudtjr
Copy link

+1

@fairnakub
Copy link

+1

@lmarcarini
Copy link

+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.

@gggiovanny
Copy link

+1, this would be very useful on Remix and new react router based projects

@zorahrel
Copy link

+1

@BensThoughts
Copy link

+1, same using remix.

@pete-willard
Copy link

Yes please +1

@alojzy231
Copy link

+1

1 similar comment
@mohamedanwer123
Copy link

+1

@cdeutsch
Copy link

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.

@DrakkoFire
Copy link

Any news on this ?

@elving
Copy link

elving commented Nov 11, 2022

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>
}

@DrakkoFire
Copy link

Pretty convenient, thanks @elving :)

@karlhorky
Copy link
Contributor

karlhorky commented Feb 15, 2023

I asked for a similar feature for Response and Request interfaces to accept generic type parameters: #52777

@karlhorky
Copy link
Contributor

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 })

TypeScript Playground

@twicer-is-coder
Copy link

+1

3 similar comments
@adophilus
Copy link

+1

@verber33
Copy link

+1

@zonzujiro
Copy link

+1

@ayech0x2
Copy link

ayech0x2 commented Jun 3, 2024

+1

@k1eu
Copy link

k1eu commented Aug 12, 2024

You can check out my library for just doing this thing :) Its built on the FormData primitive so can be used in replacement of native FormData :)
https://github.com/k1eu/typed-formdata

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests